@planningcenter/topbar 11.2.5 → 12.0.0-qa-832.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/topbar.js +935 -50
- package/dist/topbar.js.map +1 -1
- package/dist/topbar.modern.js +936 -51
- package/dist/topbar.modern.js.map +1 -1
- package/dist/topbar.module.js +936 -51
- package/dist/topbar.module.js.map +1 -1
- package/dist/topbar.umd.js +938 -54
- package/dist/topbar.umd.js.map +1 -1
- package/dist/types/index.d.ts +2 -2
- package/package.json +6 -4
package/dist/topbar.umd.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
(function (global, factory) {
|
|
2
|
-
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('@planningcenter/tapestry'), require('react'), require('@planningcenter/url'), require('@planningcenter/icons/paths/general'), require('@planningcenter/jolt-client'), require('use-sync-external-store/shim'), require('@planningcenter/tapestry-react'), require('@planningcenter/add-ons'), require('@planningcenter/icons/paths/church-center'), require('@planningcenter/icons/paths/people'), require('@planningcenter/icons/paths/toolbar'), require('react-calendly'), require('algoliasearch'), require('react-instantsearch-core'), require('react-transition-group'), require('@planningcenter/datetime-fmt')) :
|
|
3
|
-
typeof define === 'function' && define.amd ? define(['exports', '@planningcenter/tapestry', 'react', '@planningcenter/url', '@planningcenter/icons/paths/general', '@planningcenter/jolt-client', 'use-sync-external-store/shim', '@planningcenter/tapestry-react', '@planningcenter/add-ons', '@planningcenter/icons/paths/church-center', '@planningcenter/icons/paths/people', '@planningcenter/icons/paths/toolbar', 'react-calendly', 'algoliasearch', 'react-instantsearch-core', 'react-transition-group', '@planningcenter/datetime-fmt'], factory) :
|
|
4
|
-
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory((global.PLANNINGCENTER = global.PLANNINGCENTER || {}, global.PLANNINGCENTER.Topbar = {}), global.tapestry, global.react, global.pcoUrl, global.general, global.Jolt, global.shim, global.tapestryReact, global.addOns, global.churchCenter, global.people, global.toolbar, global.reactCalendly, global.algoliasearch, global.reactInstantsearchCore, global.reactTransitionGroup, global.datetimeFmt));
|
|
5
|
-
})(this, (function (exports, tapestry, React, pcoUrl$1, general, Jolt, shim, tapestryReact, addOns, churchCenter, people, toolbar, reactCalendly, algoliasearch, reactInstantsearchCore, reactTransitionGroup, datetimeFmt) { 'use strict';
|
|
2
|
+
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('@planningcenter/tapestry'), require('react'), require('@planningcenter/url'), require('@planningcenter/icons/paths/general'), require('@planningcenter/jolt-client'), require('use-sync-external-store/shim'), require('react/jsx-runtime'), require('@planningcenter/tapestry-react'), require('@planningcenter/add-ons'), require('@planningcenter/icons/paths/church-center'), require('@planningcenter/icons/paths/people'), require('@planningcenter/icons/paths/toolbar'), require('react-calendly'), require('algoliasearch'), require('react-instantsearch-core'), require('react-transition-group'), require('@planningcenter/datetime-fmt')) :
|
|
3
|
+
typeof define === 'function' && define.amd ? define(['exports', '@planningcenter/tapestry', 'react', '@planningcenter/url', '@planningcenter/icons/paths/general', '@planningcenter/jolt-client', 'use-sync-external-store/shim', 'react/jsx-runtime', '@planningcenter/tapestry-react', '@planningcenter/add-ons', '@planningcenter/icons/paths/church-center', '@planningcenter/icons/paths/people', '@planningcenter/icons/paths/toolbar', 'react-calendly', 'algoliasearch', 'react-instantsearch-core', 'react-transition-group', '@planningcenter/datetime-fmt'], factory) :
|
|
4
|
+
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory((global.PLANNINGCENTER = global.PLANNINGCENTER || {}, global.PLANNINGCENTER.Topbar = {}), global.tapestry, global.react, global.pcoUrl, global.general, global.Jolt, global.shim, null, global.tapestryReact, global.addOns, global.churchCenter, global.people, global.toolbar, global.reactCalendly, global.algoliasearch, global.reactInstantsearchCore, global.reactTransitionGroup, global.datetimeFmt));
|
|
5
|
+
})(this, (function (exports, tapestry, React, pcoUrl$1, general, Jolt, shim, jsxRuntime, tapestryReact, addOns, churchCenter, people, toolbar, reactCalendly, algoliasearch, reactInstantsearchCore, reactTransitionGroup, datetimeFmt) { 'use strict';
|
|
6
6
|
|
|
7
7
|
function _interopNamespaceDefault(e) {
|
|
8
8
|
var n = Object.create(null);
|
|
@@ -951,8 +951,8 @@
|
|
|
951
951
|
const notificationsEnabled = !window.notificationsDisabled;
|
|
952
952
|
const organizationTimeZone = ((_k = (_j = topbarResponse === null || topbarResponse === void 0 ? void 0 : topbarResponse.data) === null || _j === void 0 ? void 0 : _j.attributes) === null || _k === void 0 ? void 0 : _k.org_time_zone) || "America/Los_Angeles";
|
|
953
953
|
const algoliaSearchAuth = {
|
|
954
|
-
algoliaApplicationId: (_m = (_l = topbarResponse === null || topbarResponse === void 0 ? void 0 : topbarResponse.data) === null || _l === void 0 ? void 0 : _l.attributes) === null || _m === void 0 ? void 0 : _m.
|
|
955
|
-
algoliaSearchOnlyApiKey: (_p = (_o = topbarResponse === null || topbarResponse === void 0 ? void 0 : topbarResponse.data) === null || _o === void 0 ? void 0 : _o.attributes) === null || _p === void 0 ? void 0 : _p.
|
|
954
|
+
algoliaApplicationId: (_m = (_l = topbarResponse === null || topbarResponse === void 0 ? void 0 : topbarResponse.data) === null || _l === void 0 ? void 0 : _l.attributes) === null || _m === void 0 ? void 0 : _m.algolia_help_center_application_identifier,
|
|
955
|
+
algoliaSearchOnlyApiKey: (_p = (_o = topbarResponse === null || topbarResponse === void 0 ? void 0 : topbarResponse.data) === null || _o === void 0 ? void 0 : _o.attributes) === null || _p === void 0 ? void 0 : _p.algolia_help_center_search_key
|
|
956
956
|
};
|
|
957
957
|
const joltPromise = React.useMemo(() => {
|
|
958
958
|
return resolveJoltPromise({
|
|
@@ -2235,10 +2235,750 @@
|
|
|
2235
2235
|
const appsMenuFormatter = apps => sortByAttribute("name")(apps).reduce((acc, item) => (item.name || "") === "Church Center" || (item.name || "") === "API" ? acc : item.name === "Home" ? [item, ...acc] : [...acc, item], []);
|
|
2236
2236
|
const connectedPeopleMenuFormatter = connectedPeople => connectedPeople.slice().sort((a, b) => a.attributes["organization_name"].localeCompare(b.attributes["organization_name"]));
|
|
2237
2237
|
|
|
2238
|
+
// Generated by scripts/write-version.cjs from package.json. Do not edit.
|
|
2239
|
+
const BRIDGE_VERSION = "0.0.7-qa-41.0";
|
|
2240
|
+
|
|
2241
|
+
/**
|
|
2242
|
+
* PCA_BRIDGE_BUS_INIT — the script injected via
|
|
2243
|
+
* `injectedJavaScriptBeforeContentLoaded` before any web content loads.
|
|
2244
|
+
*
|
|
2245
|
+
* Installs `window.__pcaBridge` in the WebView's JS context. The bus:
|
|
2246
|
+
* - is per-WebView-context-by-construction (each WebView has its own JS
|
|
2247
|
+
* context; the global lives only in that context)
|
|
2248
|
+
* - accepts multiple registered handlers per reserved message type so
|
|
2249
|
+
* multiple Bridge instances in the same WebView can coexist
|
|
2250
|
+
* - buffers messages of a type with no registered handlers for up to 5s
|
|
2251
|
+
* - pings native with `__bridge/busReady` after install so the native
|
|
2252
|
+
* transport can flush its outgoing queue and trigger `reannounceLocal()`
|
|
2253
|
+
*
|
|
2254
|
+
* The bus runs as an IIFE so consumers can dispatch and register against it
|
|
2255
|
+
* immediately. Internal state (buffers, handlers) is closure-private.
|
|
2256
|
+
*/
|
|
2257
|
+
|
|
2258
|
+
const PCA_BRIDGE_BUS_INIT = `
|
|
2259
|
+
(function () {
|
|
2260
|
+
if (typeof window === 'undefined') return
|
|
2261
|
+
if (window.__pcaBridge) return // idempotent in case it's injected twice
|
|
2262
|
+
|
|
2263
|
+
var BRIDGE_VERSION = ${JSON.stringify(BRIDGE_VERSION)}
|
|
2264
|
+
var BUFFER_TTL_MS = 5000
|
|
2265
|
+
var handlers = Object.create(null) // type -> Array<fn>
|
|
2266
|
+
var buffer = Object.create(null) // type -> Array<{msg, receivedAt}>
|
|
2267
|
+
var logger = {
|
|
2268
|
+
debug: function () {},
|
|
2269
|
+
warn: function (msg, ctx) {
|
|
2270
|
+
try { console.warn('[pca-bridge]', msg, ctx) } catch (e) {}
|
|
2271
|
+
},
|
|
2272
|
+
error: function (msg, ctx) {
|
|
2273
|
+
try { console.error('[pca-bridge]', msg, ctx) } catch (e) {}
|
|
2274
|
+
}
|
|
2275
|
+
}
|
|
2276
|
+
|
|
2277
|
+
function now() { return Date.now() }
|
|
2278
|
+
|
|
2279
|
+
function sweepBuffer() {
|
|
2280
|
+
var cutoff = now() - BUFFER_TTL_MS
|
|
2281
|
+
var dropped = []
|
|
2282
|
+
var hasAny = false
|
|
2283
|
+
for (var type in buffer) {
|
|
2284
|
+
var entries = buffer[type]
|
|
2285
|
+
var kept = []
|
|
2286
|
+
for (var i = 0; i < entries.length; i++) {
|
|
2287
|
+
if (entries[i].receivedAt >= cutoff) {
|
|
2288
|
+
kept.push(entries[i])
|
|
2289
|
+
} else {
|
|
2290
|
+
dropped.push({ type: type, ageMs: now() - entries[i].receivedAt })
|
|
2291
|
+
}
|
|
2292
|
+
}
|
|
2293
|
+
if (kept.length === 0) delete buffer[type]
|
|
2294
|
+
else {
|
|
2295
|
+
buffer[type] = kept
|
|
2296
|
+
hasAny = true
|
|
2297
|
+
}
|
|
2298
|
+
}
|
|
2299
|
+
if (dropped.length > 0 && window.ReactNativeWebView && typeof window.ReactNativeWebView.postMessage === 'function') {
|
|
2300
|
+
try {
|
|
2301
|
+
window.ReactNativeWebView.postMessage(JSON.stringify({
|
|
2302
|
+
type: '__bridge/drop',
|
|
2303
|
+
bridgeVersion: BRIDGE_VERSION,
|
|
2304
|
+
messages: dropped
|
|
2305
|
+
}))
|
|
2306
|
+
} catch (e) {}
|
|
2307
|
+
}
|
|
2308
|
+
// Stop ticking when the buffer drains; ensureSweep() restarts on demand.
|
|
2309
|
+
if (!hasAny && sweepInterval !== null) {
|
|
2310
|
+
clearInterval(sweepInterval)
|
|
2311
|
+
sweepInterval = null
|
|
2312
|
+
}
|
|
2313
|
+
}
|
|
2314
|
+
|
|
2315
|
+
var sweepInterval = null
|
|
2316
|
+
function ensureSweep() {
|
|
2317
|
+
if (sweepInterval !== null) return
|
|
2318
|
+
sweepInterval = setInterval(sweepBuffer, 1000)
|
|
2319
|
+
}
|
|
2320
|
+
|
|
2321
|
+
function register(type, fn) {
|
|
2322
|
+
if (!handlers[type]) handlers[type] = []
|
|
2323
|
+
handlers[type].push(fn)
|
|
2324
|
+
// Drain any buffered messages for this type, applying the same TTL the
|
|
2325
|
+
// background sweep uses so entries that already expired don't get
|
|
2326
|
+
// delivered just because sweep hasn't run yet.
|
|
2327
|
+
var pending = buffer[type]
|
|
2328
|
+
if (pending && pending.length > 0) {
|
|
2329
|
+
delete buffer[type]
|
|
2330
|
+
var cutoff = now() - BUFFER_TTL_MS
|
|
2331
|
+
for (var i = 0; i < pending.length; i++) {
|
|
2332
|
+
if (pending[i].receivedAt < cutoff) continue
|
|
2333
|
+
try { fn(pending[i].msg) } catch (e) {
|
|
2334
|
+
logger.error('Handler threw while draining buffer', { type: type, error: e })
|
|
2335
|
+
}
|
|
2336
|
+
}
|
|
2337
|
+
}
|
|
2338
|
+
}
|
|
2339
|
+
|
|
2340
|
+
function unregister(type, fn) {
|
|
2341
|
+
var arr = handlers[type]
|
|
2342
|
+
if (!arr) return
|
|
2343
|
+
var idx = arr.indexOf(fn)
|
|
2344
|
+
if (idx >= 0) arr.splice(idx, 1)
|
|
2345
|
+
if (arr.length === 0) delete handlers[type]
|
|
2346
|
+
}
|
|
2347
|
+
|
|
2348
|
+
function dispatch(msg) {
|
|
2349
|
+
if (!msg || typeof msg !== 'object' || typeof msg.type !== 'string') return
|
|
2350
|
+
var type = msg.type
|
|
2351
|
+
var arr = handlers[type]
|
|
2352
|
+
if (arr && arr.length > 0) {
|
|
2353
|
+
// Snapshot the array to avoid issues if a handler registers/unregisters during dispatch
|
|
2354
|
+
var snapshot = arr.slice()
|
|
2355
|
+
for (var i = 0; i < snapshot.length; i++) {
|
|
2356
|
+
try { snapshot[i](msg) } catch (e) {
|
|
2357
|
+
logger.error('Handler threw during dispatch', { type: type, error: e })
|
|
2358
|
+
}
|
|
2359
|
+
}
|
|
2360
|
+
} else {
|
|
2361
|
+
if (!buffer[type]) buffer[type] = []
|
|
2362
|
+
buffer[type].push({ msg: msg, receivedAt: now() })
|
|
2363
|
+
ensureSweep()
|
|
2364
|
+
}
|
|
2365
|
+
}
|
|
2366
|
+
|
|
2367
|
+
function setLogger(next) {
|
|
2368
|
+
// Validate both methods the bus actually calls; a partial logger would
|
|
2369
|
+
// throw TypeError when dispatch/register tries to invoke a missing one.
|
|
2370
|
+
if (next && typeof next.warn === 'function' && typeof next.error === 'function') {
|
|
2371
|
+
logger = next
|
|
2372
|
+
}
|
|
2373
|
+
}
|
|
2374
|
+
|
|
2375
|
+
window.__pcaBridge = {
|
|
2376
|
+
register: register,
|
|
2377
|
+
unregister: unregister,
|
|
2378
|
+
dispatch: dispatch,
|
|
2379
|
+
setLogger: setLogger
|
|
2380
|
+
}
|
|
2381
|
+
|
|
2382
|
+
// Ping native: the bus is ready in this JS context. Native transport
|
|
2383
|
+
// listens for this to (a) flush its outgoing queue and (b) call
|
|
2384
|
+
// bridge.reannounceLocal() so the freshly-loaded web context learns about
|
|
2385
|
+
// already-exposed local actions/values.
|
|
2386
|
+
try {
|
|
2387
|
+
if (window.ReactNativeWebView && typeof window.ReactNativeWebView.postMessage === 'function') {
|
|
2388
|
+
window.ReactNativeWebView.postMessage(JSON.stringify({ type: '__bridge/busReady', bridgeVersion: BRIDGE_VERSION }))
|
|
2389
|
+
}
|
|
2390
|
+
} catch (e) {}
|
|
2391
|
+
})();
|
|
2392
|
+
true;
|
|
2393
|
+
`;
|
|
2394
|
+
|
|
2395
|
+
class TimeoutError extends Error {
|
|
2396
|
+
constructor(name, timeoutMs) {
|
|
2397
|
+
super(`Call to "${name}" timed out after ${timeoutMs}ms`);
|
|
2398
|
+
this.name = 'TimeoutError';
|
|
2399
|
+
}
|
|
2400
|
+
}
|
|
2401
|
+
class HandlerError extends Error {
|
|
2402
|
+
constructor(name, code, originalMessage) {
|
|
2403
|
+
super(`Handler for "${name}" threw [${code}]: ${originalMessage}`);
|
|
2404
|
+
this.name = 'HandlerError';
|
|
2405
|
+
}
|
|
2406
|
+
}
|
|
2407
|
+
class DisposedError extends Error {
|
|
2408
|
+
constructor(name) {
|
|
2409
|
+
super(`Bridge disposed while call to "${name}" was in flight`);
|
|
2410
|
+
this.name = 'DisposedError';
|
|
2411
|
+
}
|
|
2412
|
+
}
|
|
2413
|
+
|
|
2414
|
+
let idCounter = 0;
|
|
2415
|
+
function generateId() {
|
|
2416
|
+
idCounter += 1;
|
|
2417
|
+
return `pca-bridge-${idCounter}`;
|
|
2418
|
+
}
|
|
2419
|
+
const noopLogger = {
|
|
2420
|
+
debug: () => {},
|
|
2421
|
+
warn: () => {},
|
|
2422
|
+
error: () => {}
|
|
2423
|
+
};
|
|
2424
|
+
function isIndexable(x) {
|
|
2425
|
+
return typeof x === 'object' && x !== null;
|
|
2426
|
+
}
|
|
2427
|
+
|
|
2428
|
+
/**
|
|
2429
|
+
* Shallow equality, one level deep. Used by `BridgeCore.publish` to dedup
|
|
2430
|
+
* re-emissions of new-but-content-equal values (e.g., a freshly-built
|
|
2431
|
+
* `modalButtons` array whose contents didn't change between renders).
|
|
2432
|
+
*/
|
|
2433
|
+
function shallowEqual(a, b) {
|
|
2434
|
+
const sameRef = Object.is(a, b);
|
|
2435
|
+
const bothComparableObjects = isIndexable(a) && isIndexable(b) && Array.isArray(a) === Array.isArray(b);
|
|
2436
|
+
let result;
|
|
2437
|
+
if (sameRef) {
|
|
2438
|
+
result = true;
|
|
2439
|
+
} else if (!bothComparableObjects) {
|
|
2440
|
+
result = false;
|
|
2441
|
+
} else {
|
|
2442
|
+
const keys = Object.keys(a);
|
|
2443
|
+
result = keys.length === Object.keys(b).length && keys.every(k => Object.prototype.hasOwnProperty.call(b, k) && Object.is(a[k], b[k]));
|
|
2444
|
+
}
|
|
2445
|
+
return result;
|
|
2446
|
+
}
|
|
2447
|
+
function isWireEnvelope(parsed) {
|
|
2448
|
+
return typeof parsed === 'object' && parsed !== null && 'type' in parsed && typeof parsed.type === 'string' && parsed.type.startsWith('__bridge/') && 'bridgeVersion' in parsed && typeof parsed.bridgeVersion === 'string';
|
|
2449
|
+
}
|
|
2450
|
+
function parseWireEnvelope(rawData) {
|
|
2451
|
+
let parsed;
|
|
2452
|
+
try {
|
|
2453
|
+
parsed = JSON.parse(rawData);
|
|
2454
|
+
} catch {
|
|
2455
|
+
parsed = null;
|
|
2456
|
+
}
|
|
2457
|
+
return isWireEnvelope(parsed) ? parsed : null;
|
|
2458
|
+
}
|
|
2459
|
+
|
|
2460
|
+
const DEFAULT_TIMEOUT_MS = 5000;
|
|
2461
|
+
const INCOMING_CALL_QUEUE_TTL_MS = 5000;
|
|
2462
|
+
const INCOMING_CALL_QUEUE_MAX_PER_ACTION = 32;
|
|
2463
|
+
class BridgeCore {
|
|
2464
|
+
injectedJavaScriptBeforeContentLoaded = PCA_BRIDGE_BUS_INIT;
|
|
2465
|
+
localVersion = BRIDGE_VERSION;
|
|
2466
|
+
// `never` arg type lets us widen any user-defined `LocalActions[K]` callable
|
|
2467
|
+
// with a single `as` cast inside `expose()`.
|
|
2468
|
+
localActions = new Map();
|
|
2469
|
+
localValues = new Map();
|
|
2470
|
+
remoteActions = new Set();
|
|
2471
|
+
remoteValues = new Map();
|
|
2472
|
+
actionListeners = new Map();
|
|
2473
|
+
valueListeners = new Map();
|
|
2474
|
+
pendingCalls = new Map();
|
|
2475
|
+
callQueue = new Map();
|
|
2476
|
+
|
|
2477
|
+
// Private backing field + public getter so the `readonly` declared on the
|
|
2478
|
+
// Bridge interface is actually enforced — not just at the interface level
|
|
2479
|
+
// but for any code holding a concrete BridgeCore reference.
|
|
2480
|
+
_remoteVersion = undefined;
|
|
2481
|
+
get remoteVersion() {
|
|
2482
|
+
return this._remoteVersion;
|
|
2483
|
+
}
|
|
2484
|
+
disposed = false;
|
|
2485
|
+
constructor(options) {
|
|
2486
|
+
const sendRaw = options.send;
|
|
2487
|
+
this.send = msg => {
|
|
2488
|
+
sendRaw({
|
|
2489
|
+
...msg,
|
|
2490
|
+
bridgeVersion: BRIDGE_VERSION
|
|
2491
|
+
});
|
|
2492
|
+
};
|
|
2493
|
+
this.logger = options.logger ?? noopLogger;
|
|
2494
|
+
this.defaultTimeoutMs = options.defaultTimeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
2495
|
+
this.now = options.now ?? Date.now;
|
|
2496
|
+
}
|
|
2497
|
+
call(name, args, options) {
|
|
2498
|
+
const requestId = generateId();
|
|
2499
|
+
const timeoutMs = options?.timeoutMs ?? this.defaultTimeoutMs;
|
|
2500
|
+
return new Promise((resolve, reject) => {
|
|
2501
|
+
const timer = setTimeout(() => {
|
|
2502
|
+
if (this.pendingCalls.delete(requestId)) {
|
|
2503
|
+
reject(new TimeoutError(name, timeoutMs));
|
|
2504
|
+
}
|
|
2505
|
+
}, timeoutMs);
|
|
2506
|
+
// Heterogeneous map: cast the typed resolve to the loose storage slot.
|
|
2507
|
+
this.pendingCalls.set(requestId, {
|
|
2508
|
+
name,
|
|
2509
|
+
resolve: resolve,
|
|
2510
|
+
reject,
|
|
2511
|
+
timer
|
|
2512
|
+
});
|
|
2513
|
+
this.send({
|
|
2514
|
+
type: '__bridge/call',
|
|
2515
|
+
requestId,
|
|
2516
|
+
name,
|
|
2517
|
+
args
|
|
2518
|
+
});
|
|
2519
|
+
});
|
|
2520
|
+
}
|
|
2521
|
+
expose(name, handler) {
|
|
2522
|
+
if (this.localActions.has(name)) {
|
|
2523
|
+
this.logger.warn('Action already exposed; last-write-wins (the previous handler is gone)', {
|
|
2524
|
+
name
|
|
2525
|
+
});
|
|
2526
|
+
}
|
|
2527
|
+
this.localActions.set(name, handler);
|
|
2528
|
+
this.send({
|
|
2529
|
+
type: '__bridge/exposed',
|
|
2530
|
+
name,
|
|
2531
|
+
kind: 'action'
|
|
2532
|
+
});
|
|
2533
|
+
const queued = this.callQueue.get(name);
|
|
2534
|
+
if (queued) {
|
|
2535
|
+
this.callQueue.delete(name);
|
|
2536
|
+
const cutoff = this.now() - INCOMING_CALL_QUEUE_TTL_MS;
|
|
2537
|
+
for (const entry of queued) {
|
|
2538
|
+
if (entry.receivedAt >= cutoff) {
|
|
2539
|
+
void this.invokeLocalHandler(entry.msg);
|
|
2540
|
+
} else {
|
|
2541
|
+
this.logger.warn('Queued incoming call expired before handler registered', {
|
|
2542
|
+
name,
|
|
2543
|
+
ageMs: this.now() - entry.receivedAt
|
|
2544
|
+
});
|
|
2545
|
+
this.rejectQueued(entry, 'expired');
|
|
2546
|
+
}
|
|
2547
|
+
}
|
|
2548
|
+
}
|
|
2549
|
+
}
|
|
2550
|
+
unexpose(name) {
|
|
2551
|
+
if (this.localActions.delete(name)) {
|
|
2552
|
+
this.send({
|
|
2553
|
+
type: '__bridge/unexposed',
|
|
2554
|
+
name,
|
|
2555
|
+
kind: 'action'
|
|
2556
|
+
});
|
|
2557
|
+
}
|
|
2558
|
+
}
|
|
2559
|
+
hasRemoteAction(name) {
|
|
2560
|
+
return this.remoteActions.has(name);
|
|
2561
|
+
}
|
|
2562
|
+
onRemoteAction(name, callback) {
|
|
2563
|
+
let set = this.actionListeners.get(name);
|
|
2564
|
+
if (!set) {
|
|
2565
|
+
set = new Set();
|
|
2566
|
+
this.actionListeners.set(name, set);
|
|
2567
|
+
}
|
|
2568
|
+
set.add(callback);
|
|
2569
|
+
return () => {
|
|
2570
|
+
const current = this.actionListeners.get(name);
|
|
2571
|
+
if (current) {
|
|
2572
|
+
current.delete(callback);
|
|
2573
|
+
if (current.size === 0) {
|
|
2574
|
+
this.actionListeners.delete(name);
|
|
2575
|
+
}
|
|
2576
|
+
}
|
|
2577
|
+
};
|
|
2578
|
+
}
|
|
2579
|
+
publish(name, value) {
|
|
2580
|
+
// Skip wire send if content unchanged — callers may publish on every
|
|
2581
|
+
// render with a fresh-but-content-equal value.
|
|
2582
|
+
if (!shallowEqual(this.localValues.get(name), value)) {
|
|
2583
|
+
this.localValues.set(name, value);
|
|
2584
|
+
this.send({
|
|
2585
|
+
type: '__bridge/publish',
|
|
2586
|
+
name,
|
|
2587
|
+
value
|
|
2588
|
+
});
|
|
2589
|
+
}
|
|
2590
|
+
}
|
|
2591
|
+
unpublish(name) {
|
|
2592
|
+
if (this.localValues.delete(name)) {
|
|
2593
|
+
this.send({
|
|
2594
|
+
type: '__bridge/unexposed',
|
|
2595
|
+
name,
|
|
2596
|
+
kind: 'value'
|
|
2597
|
+
});
|
|
2598
|
+
}
|
|
2599
|
+
}
|
|
2600
|
+
seedLocalValue(name, value) {
|
|
2601
|
+
this.localValues.set(name, value);
|
|
2602
|
+
}
|
|
2603
|
+
getRemoteValue(name) {
|
|
2604
|
+
return this.remoteValues.get(name);
|
|
2605
|
+
}
|
|
2606
|
+
onRemoteValue(name, callback) {
|
|
2607
|
+
let set = this.valueListeners.get(name);
|
|
2608
|
+
if (!set) {
|
|
2609
|
+
set = new Set();
|
|
2610
|
+
this.valueListeners.set(name, set);
|
|
2611
|
+
}
|
|
2612
|
+
const wrapped = callback;
|
|
2613
|
+
set.add(wrapped);
|
|
2614
|
+
return () => {
|
|
2615
|
+
const current = this.valueListeners.get(name);
|
|
2616
|
+
if (current) {
|
|
2617
|
+
current.delete(wrapped);
|
|
2618
|
+
if (current.size === 0) {
|
|
2619
|
+
this.valueListeners.delete(name);
|
|
2620
|
+
}
|
|
2621
|
+
}
|
|
2622
|
+
};
|
|
2623
|
+
}
|
|
2624
|
+
reannounceLocal() {
|
|
2625
|
+
for (const name of this.localActions.keys()) {
|
|
2626
|
+
this.send({
|
|
2627
|
+
type: '__bridge/exposed',
|
|
2628
|
+
name,
|
|
2629
|
+
kind: 'action'
|
|
2630
|
+
});
|
|
2631
|
+
}
|
|
2632
|
+
for (const [name, value] of this.localValues) {
|
|
2633
|
+
this.send({
|
|
2634
|
+
type: '__bridge/exposed',
|
|
2635
|
+
name,
|
|
2636
|
+
kind: 'value',
|
|
2637
|
+
currentValue: value
|
|
2638
|
+
});
|
|
2639
|
+
}
|
|
2640
|
+
}
|
|
2641
|
+
|
|
2642
|
+
/**
|
|
2643
|
+
* Drop remote-side cached state. Called by transports when the remote JS
|
|
2644
|
+
* context is about to be torn down (e.g., WebView reload): the new context
|
|
2645
|
+
* will re-announce what it actually exposes, and stale entries would
|
|
2646
|
+
* otherwise silently mask removed actions/values.
|
|
2647
|
+
*/
|
|
2648
|
+
clearRemoteState() {
|
|
2649
|
+
for (const name of this.remoteActions) {
|
|
2650
|
+
this.notifySet(this.actionListeners, name, false, 'action availability');
|
|
2651
|
+
}
|
|
2652
|
+
this.remoteActions.clear();
|
|
2653
|
+
for (const name of this.remoteValues.keys()) {
|
|
2654
|
+
this.notifySet(this.valueListeners, name, undefined, 'remote value change');
|
|
2655
|
+
}
|
|
2656
|
+
this.remoteValues.clear();
|
|
2657
|
+
this._remoteVersion = undefined;
|
|
2658
|
+
}
|
|
2659
|
+
handleMessage(rawData) {
|
|
2660
|
+
const msg = parseWireEnvelope(rawData);
|
|
2661
|
+
let consumed = false;
|
|
2662
|
+
if (msg) {
|
|
2663
|
+
this.handleIncomingMessage(msg);
|
|
2664
|
+
consumed = true;
|
|
2665
|
+
}
|
|
2666
|
+
return consumed;
|
|
2667
|
+
}
|
|
2668
|
+
applyLogger(logger) {
|
|
2669
|
+
if (this.logger === noopLogger) {
|
|
2670
|
+
this.logger = logger;
|
|
2671
|
+
return true;
|
|
2672
|
+
}
|
|
2673
|
+
return false;
|
|
2674
|
+
}
|
|
2675
|
+
handleLoadStart() {}
|
|
2676
|
+
handleLoadEnd() {}
|
|
2677
|
+
handleNavigationStateChange(navState) {
|
|
2678
|
+
if (!navState.loading) {
|
|
2679
|
+
this.handleLoadEnd();
|
|
2680
|
+
}
|
|
2681
|
+
}
|
|
2682
|
+
handleIncomingMessage(msg) {
|
|
2683
|
+
if (!this.disposed) {
|
|
2684
|
+
this._remoteVersion = msg.bridgeVersion;
|
|
2685
|
+
switch (msg.type) {
|
|
2686
|
+
case '__bridge/call':
|
|
2687
|
+
void this.routeIncomingCall(msg);
|
|
2688
|
+
break;
|
|
2689
|
+
case '__bridge/response':
|
|
2690
|
+
this.handleIncomingResponse(msg);
|
|
2691
|
+
break;
|
|
2692
|
+
case '__bridge/exposed':
|
|
2693
|
+
this.handleIncomingExposed(msg);
|
|
2694
|
+
break;
|
|
2695
|
+
case '__bridge/unexposed':
|
|
2696
|
+
this.handleIncomingUnexposed(msg);
|
|
2697
|
+
break;
|
|
2698
|
+
case '__bridge/publish':
|
|
2699
|
+
this.handleIncomingPublish(msg);
|
|
2700
|
+
break;
|
|
2701
|
+
case '__bridge/busReady':
|
|
2702
|
+
case '__bridge/drop':
|
|
2703
|
+
break;
|
|
2704
|
+
default:
|
|
2705
|
+
{
|
|
2706
|
+
// parseWireEnvelope admits any `__bridge/*` type; this catches
|
|
2707
|
+
// future message types a remote on a newer bridge sends to us.
|
|
2708
|
+
const unknown = msg;
|
|
2709
|
+
this.logger.warn('Received unknown bridge message type; ignoring', {
|
|
2710
|
+
type: unknown.type,
|
|
2711
|
+
remoteVersion: this.remoteVersion
|
|
2712
|
+
});
|
|
2713
|
+
break;
|
|
2714
|
+
}
|
|
2715
|
+
}
|
|
2716
|
+
}
|
|
2717
|
+
}
|
|
2718
|
+
get debug() {
|
|
2719
|
+
return {
|
|
2720
|
+
listLocalActions: () => Array.from(this.localActions.keys()),
|
|
2721
|
+
listRemoteActions: () => Array.from(this.remoteActions),
|
|
2722
|
+
listLocalValues: () => Array.from(this.localValues.keys()),
|
|
2723
|
+
listRemoteValues: () => Array.from(this.remoteValues.keys())
|
|
2724
|
+
};
|
|
2725
|
+
}
|
|
2726
|
+
dispose() {
|
|
2727
|
+
if (!this.disposed) {
|
|
2728
|
+
this.disposed = true;
|
|
2729
|
+
for (const pending of this.pendingCalls.values()) {
|
|
2730
|
+
clearTimeout(pending.timer);
|
|
2731
|
+
pending.reject(new DisposedError(pending.name));
|
|
2732
|
+
}
|
|
2733
|
+
this.pendingCalls.clear();
|
|
2734
|
+
this.callQueue.clear();
|
|
2735
|
+
}
|
|
2736
|
+
}
|
|
2737
|
+
async routeIncomingCall(msg) {
|
|
2738
|
+
if (this.localActions.has(msg.name)) {
|
|
2739
|
+
await this.invokeLocalHandler(msg);
|
|
2740
|
+
return;
|
|
2741
|
+
}
|
|
2742
|
+
let queued = this.callQueue.get(msg.name);
|
|
2743
|
+
if (!queued) {
|
|
2744
|
+
queued = [];
|
|
2745
|
+
this.callQueue.set(msg.name, queued);
|
|
2746
|
+
}
|
|
2747
|
+
// Evict TTL-expired and over-capacity entries so a remote that spams an
|
|
2748
|
+
// unknown action can't grow the queue without bound. Each evicted call
|
|
2749
|
+
// gets an error response so the caller fails fast.
|
|
2750
|
+
const cutoff = this.now() - INCOMING_CALL_QUEUE_TTL_MS;
|
|
2751
|
+
while (queued.length > 0) {
|
|
2752
|
+
const head = queued[0];
|
|
2753
|
+
if (!head || head.receivedAt >= cutoff) break;
|
|
2754
|
+
queued.shift();
|
|
2755
|
+
this.rejectQueued(head, 'expired');
|
|
2756
|
+
}
|
|
2757
|
+
while (queued.length >= INCOMING_CALL_QUEUE_MAX_PER_ACTION) {
|
|
2758
|
+
const evicted = queued.shift();
|
|
2759
|
+
if (!evicted) break;
|
|
2760
|
+
this.rejectQueued(evicted, 'queue_full');
|
|
2761
|
+
}
|
|
2762
|
+
queued.push({
|
|
2763
|
+
msg,
|
|
2764
|
+
receivedAt: this.now()
|
|
2765
|
+
});
|
|
2766
|
+
}
|
|
2767
|
+
rejectQueued(entry, code) {
|
|
2768
|
+
this.send({
|
|
2769
|
+
type: '__bridge/response',
|
|
2770
|
+
requestId: entry.msg.requestId,
|
|
2771
|
+
ok: false,
|
|
2772
|
+
code,
|
|
2773
|
+
message: `Queued call to "${entry.msg.name}" dropped (${code})`
|
|
2774
|
+
});
|
|
2775
|
+
}
|
|
2776
|
+
async invokeLocalHandler(msg) {
|
|
2777
|
+
// Handler may have been unregistered between queue push and drain. Reply
|
|
2778
|
+
// with an explicit error so the caller fails fast instead of timing out.
|
|
2779
|
+
const handler = this.localActions.get(msg.name);
|
|
2780
|
+
if (!handler) {
|
|
2781
|
+
this.send({
|
|
2782
|
+
type: '__bridge/response',
|
|
2783
|
+
requestId: msg.requestId,
|
|
2784
|
+
ok: false,
|
|
2785
|
+
code: 'unexposed',
|
|
2786
|
+
message: `No handler exposed for "${msg.name}"`
|
|
2787
|
+
});
|
|
2788
|
+
return;
|
|
2789
|
+
}
|
|
2790
|
+
try {
|
|
2791
|
+
const result = await handler(msg.args);
|
|
2792
|
+
this.send({
|
|
2793
|
+
type: '__bridge/response',
|
|
2794
|
+
requestId: msg.requestId,
|
|
2795
|
+
ok: true,
|
|
2796
|
+
result
|
|
2797
|
+
});
|
|
2798
|
+
} catch (error) {
|
|
2799
|
+
this.send({
|
|
2800
|
+
type: '__bridge/response',
|
|
2801
|
+
requestId: msg.requestId,
|
|
2802
|
+
ok: false,
|
|
2803
|
+
code: 'thrown',
|
|
2804
|
+
message: error instanceof Error ? error.message : String(error)
|
|
2805
|
+
});
|
|
2806
|
+
}
|
|
2807
|
+
}
|
|
2808
|
+
handleIncomingResponse(msg) {
|
|
2809
|
+
const pending = this.pendingCalls.get(msg.requestId);
|
|
2810
|
+
if (pending) {
|
|
2811
|
+
this.pendingCalls.delete(msg.requestId);
|
|
2812
|
+
clearTimeout(pending.timer);
|
|
2813
|
+
if (msg.ok) {
|
|
2814
|
+
pending.resolve(msg.result);
|
|
2815
|
+
} else {
|
|
2816
|
+
pending.reject(new HandlerError(pending.name, msg.code, msg.message));
|
|
2817
|
+
}
|
|
2818
|
+
} else {
|
|
2819
|
+
this.logger.warn('Response for unknown or already-resolved requestId', {
|
|
2820
|
+
requestId: msg.requestId
|
|
2821
|
+
});
|
|
2822
|
+
}
|
|
2823
|
+
}
|
|
2824
|
+
handleIncomingExposed(msg) {
|
|
2825
|
+
if (msg.kind === 'action') {
|
|
2826
|
+
if (!this.remoteActions.has(msg.name)) {
|
|
2827
|
+
this.remoteActions.add(msg.name);
|
|
2828
|
+
this.notifySet(this.actionListeners, msg.name, true, 'action availability');
|
|
2829
|
+
}
|
|
2830
|
+
} else {
|
|
2831
|
+
this.remoteValues.set(msg.name, msg.currentValue);
|
|
2832
|
+
this.notifySet(this.valueListeners, msg.name, msg.currentValue, 'remote value change');
|
|
2833
|
+
}
|
|
2834
|
+
}
|
|
2835
|
+
handleIncomingUnexposed(msg) {
|
|
2836
|
+
if (msg.kind === 'action') {
|
|
2837
|
+
if (this.remoteActions.delete(msg.name)) {
|
|
2838
|
+
this.notifySet(this.actionListeners, msg.name, false, 'action availability');
|
|
2839
|
+
}
|
|
2840
|
+
} else if (this.remoteValues.delete(msg.name)) {
|
|
2841
|
+
this.notifySet(this.valueListeners, msg.name, undefined, 'remote value change');
|
|
2842
|
+
}
|
|
2843
|
+
}
|
|
2844
|
+
handleIncomingPublish(msg) {
|
|
2845
|
+
this.remoteValues.set(msg.name, msg.value);
|
|
2846
|
+
this.notifySet(this.valueListeners, msg.name, msg.value, 'remote value change');
|
|
2847
|
+
}
|
|
2848
|
+
notifySet(subs, name, value, label) {
|
|
2849
|
+
const set = subs.get(name);
|
|
2850
|
+
if (set) {
|
|
2851
|
+
for (const callback of set) {
|
|
2852
|
+
try {
|
|
2853
|
+
callback(value);
|
|
2854
|
+
} catch (error) {
|
|
2855
|
+
this.logger.error(`Subscriber threw during ${label} notification`, {
|
|
2856
|
+
name,
|
|
2857
|
+
error
|
|
2858
|
+
});
|
|
2859
|
+
}
|
|
2860
|
+
}
|
|
2861
|
+
}
|
|
2862
|
+
}
|
|
2863
|
+
}
|
|
2864
|
+
|
|
2865
|
+
// Subset of window.__pcaBridge that bridge_web.ts needs.
|
|
2866
|
+
|
|
2867
|
+
const INBOUND_TYPES = ['__bridge/call', '__bridge/response', '__bridge/exposed', '__bridge/unexposed', '__bridge/publish'];
|
|
2868
|
+
class WebBridge extends BridgeCore {
|
|
2869
|
+
constructor(options = {}) {
|
|
2870
|
+
const bus = window.__pcaBridge;
|
|
2871
|
+
super({
|
|
2872
|
+
send: msg => {
|
|
2873
|
+
if (window.ReactNativeWebView) {
|
|
2874
|
+
try {
|
|
2875
|
+
window.ReactNativeWebView.postMessage(JSON.stringify(msg));
|
|
2876
|
+
} catch (error) {
|
|
2877
|
+
options.logger?.error('WebBridge postMessage failed', {
|
|
2878
|
+
error
|
|
2879
|
+
});
|
|
2880
|
+
}
|
|
2881
|
+
}
|
|
2882
|
+
},
|
|
2883
|
+
logger: options.logger,
|
|
2884
|
+
defaultTimeoutMs: options.defaultTimeoutMs
|
|
2885
|
+
});
|
|
2886
|
+
this.bus = bus;
|
|
2887
|
+
this.handlerFn = msg => {
|
|
2888
|
+
this.handleIncomingMessage(msg);
|
|
2889
|
+
};
|
|
2890
|
+
if (bus) {
|
|
2891
|
+
if (options.logger) {
|
|
2892
|
+
bus.setLogger(options.logger);
|
|
2893
|
+
}
|
|
2894
|
+
for (const type of INBOUND_TYPES) {
|
|
2895
|
+
bus.register(type, this.handlerFn);
|
|
2896
|
+
}
|
|
2897
|
+
if (window.ReactNativeWebView) {
|
|
2898
|
+
try {
|
|
2899
|
+
window.ReactNativeWebView.postMessage(JSON.stringify({
|
|
2900
|
+
type: '__bridge/busReady',
|
|
2901
|
+
bridgeVersion: BRIDGE_VERSION
|
|
2902
|
+
}));
|
|
2903
|
+
} catch (error) {
|
|
2904
|
+
options.logger?.error('WebBridge busReady post failed', {
|
|
2905
|
+
error
|
|
2906
|
+
});
|
|
2907
|
+
}
|
|
2908
|
+
}
|
|
2909
|
+
} else {
|
|
2910
|
+
options.logger?.warn('WebBridge constructed without window.__pcaBridge — bus init script may not have run');
|
|
2911
|
+
}
|
|
2912
|
+
}
|
|
2913
|
+
applyLogger(logger) {
|
|
2914
|
+
const applied = super.applyLogger(logger);
|
|
2915
|
+
if (applied && this.bus) {
|
|
2916
|
+
this.bus.setLogger(logger);
|
|
2917
|
+
}
|
|
2918
|
+
return applied;
|
|
2919
|
+
}
|
|
2920
|
+
dispose() {
|
|
2921
|
+
if (this.bus) {
|
|
2922
|
+
for (const type of INBOUND_TYPES) {
|
|
2923
|
+
this.bus.unregister(type, this.handlerFn);
|
|
2924
|
+
}
|
|
2925
|
+
}
|
|
2926
|
+
super.dispose();
|
|
2927
|
+
}
|
|
2928
|
+
}
|
|
2929
|
+
function getOrCreateWebBridge(options = {}) {
|
|
2930
|
+
if (window.__pcaWebBridge && options.logger) {
|
|
2931
|
+
window.__pcaWebBridge.applyLogger(options.logger);
|
|
2932
|
+
} else if (!window.__pcaWebBridge) {
|
|
2933
|
+
window.__pcaWebBridge = new WebBridge(options);
|
|
2934
|
+
}
|
|
2935
|
+
return window.__pcaWebBridge;
|
|
2936
|
+
}
|
|
2937
|
+
|
|
2938
|
+
const WebBridgeContext = /*#__PURE__*/React.createContext(null);
|
|
2939
|
+
function useWebBridge() {
|
|
2940
|
+
const ctxBridge = React.useContext(WebBridgeContext);
|
|
2941
|
+
if (ctxBridge) {
|
|
2942
|
+
// oxlint-disable-next-line typescript/no-unsafe-type-assertion
|
|
2943
|
+
return ctxBridge;
|
|
2944
|
+
} else {
|
|
2945
|
+
// oxlint-disable-next-line typescript/no-unsafe-type-assertion
|
|
2946
|
+
return getOrCreateWebBridge();
|
|
2947
|
+
}
|
|
2948
|
+
}
|
|
2949
|
+
function useBridgePublish(bridge, name, value) {
|
|
2950
|
+
// publish() dedups by shallow equality, so this effect is safe to re-run
|
|
2951
|
+
// on every value change without spamming wire messages.
|
|
2952
|
+
React.useEffect(() => {
|
|
2953
|
+
bridge.publish(name, value);
|
|
2954
|
+
}, [bridge, name, value]);
|
|
2955
|
+
React.useEffect(() => {
|
|
2956
|
+
return () => {
|
|
2957
|
+
bridge.unpublish(name);
|
|
2958
|
+
};
|
|
2959
|
+
}, [bridge, name]);
|
|
2960
|
+
}
|
|
2961
|
+
|
|
2238
2962
|
const useSetCurrentRouteInfo = ({
|
|
2239
2963
|
routes,
|
|
2240
2964
|
app
|
|
2241
2965
|
}) => {
|
|
2966
|
+
const bridge = useWebBridge();
|
|
2967
|
+
useBridgePublish(bridge, "routeInfo", {
|
|
2968
|
+
currentApp: app,
|
|
2969
|
+
currentRoutes: routes.map(({
|
|
2970
|
+
key,
|
|
2971
|
+
props: {
|
|
2972
|
+
to,
|
|
2973
|
+
href,
|
|
2974
|
+
active
|
|
2975
|
+
}
|
|
2976
|
+
}) => ({
|
|
2977
|
+
active,
|
|
2978
|
+
name: key,
|
|
2979
|
+
url: to !== null && to !== void 0 ? to : href
|
|
2980
|
+
}))
|
|
2981
|
+
});
|
|
2242
2982
|
React.useEffect(() => {
|
|
2243
2983
|
if (window.topbar) {
|
|
2244
2984
|
window.topbar.routeInfo = {
|
|
@@ -2258,6 +2998,24 @@
|
|
|
2258
2998
|
};
|
|
2259
2999
|
}
|
|
2260
3000
|
}, [routes, app]);
|
|
3001
|
+
// Publish info for when app goes back
|
|
3002
|
+
React.useEffect(() => window.addEventListener("pageshow", () => {
|
|
3003
|
+
bridge.publish("routeInfo", {
|
|
3004
|
+
currentApp: app,
|
|
3005
|
+
currentRoutes: routes.map(({
|
|
3006
|
+
key,
|
|
3007
|
+
props: {
|
|
3008
|
+
to,
|
|
3009
|
+
href,
|
|
3010
|
+
active
|
|
3011
|
+
}
|
|
3012
|
+
}) => ({
|
|
3013
|
+
active,
|
|
3014
|
+
name: key,
|
|
3015
|
+
url: to !== null && to !== void 0 ? to : href
|
|
3016
|
+
}))
|
|
3017
|
+
});
|
|
3018
|
+
}), []);
|
|
2261
3019
|
};
|
|
2262
3020
|
|
|
2263
3021
|
const useUpdateStore = () => React.useCallback(updates => {
|
|
@@ -3465,40 +4223,44 @@
|
|
|
3465
4223
|
return fragment ? `${base}#${fragment}` : base;
|
|
3466
4224
|
}
|
|
3467
4225
|
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
|
|
4226
|
+
// The article heading lives in the deepest populated hierarchy level (lvl0 is
|
|
4227
|
+
// always "Planning Center").
|
|
4228
|
+
const hitTitle = hierarchy => (hierarchy === null || hierarchy === void 0 ? void 0 : hierarchy.lvl6) || (hierarchy === null || hierarchy === void 0 ? void 0 : hierarchy.lvl5) || (hierarchy === null || hierarchy === void 0 ? void 0 : hierarchy.lvl4) || (hierarchy === null || hierarchy === void 0 ? void 0 : hierarchy.lvl3) || (hierarchy === null || hierarchy === void 0 ? void 0 : hierarchy.lvl2) || (hierarchy === null || hierarchy === void 0 ? void 0 : hierarchy.lvl1) || (hierarchy === null || hierarchy === void 0 ? void 0 : hierarchy.lvl0) || "";
|
|
4229
|
+
const handleSearchSelect = hit => {
|
|
4230
|
+
// `url` is always absolute (it is also the basis of objectID), so open it
|
|
4231
|
+
// directly. Bail out defensively if a record somehow arrives without one.
|
|
4232
|
+
if (!(hit === null || hit === void 0 ? void 0 : hit.url)) return;
|
|
4233
|
+
const newTab = window.open(hit.url, "_blank");
|
|
3472
4234
|
if (newTab) {
|
|
3473
4235
|
newTab.focus();
|
|
3474
4236
|
}
|
|
3475
4237
|
};
|
|
4238
|
+
// Mirrors @planningcenter/tapestry-react's Logo registry.
|
|
4239
|
+
const VALID_LOGO_NAMES = new Set(["api", "calendar", "check-ins", "church-center", "giving", "groups", "headcounts", "home", "login", "music-stand", "people", "planning-center", "publishing", "registrations", "services"]);
|
|
3476
4240
|
const HeadingIcon = ({
|
|
3477
|
-
|
|
3478
|
-
|
|
4241
|
+
currentAppName,
|
|
4242
|
+
product
|
|
3479
4243
|
}) => {
|
|
3480
|
-
const
|
|
3481
|
-
if (
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
|
|
3485
|
-
});
|
|
3486
|
-
} else if (currentAppName === "Accounts") {
|
|
4244
|
+
const appName = product || currentAppName;
|
|
4245
|
+
if (!appName) return null;
|
|
4246
|
+
const logoName = appName.toLowerCase().replace(/\s+/g, "-");
|
|
4247
|
+
// Accounts has no tapestry Logo, so use the cog icon
|
|
4248
|
+
if (logoName === "accounts") {
|
|
3487
4249
|
return /*#__PURE__*/React.createElement(tapestryReact.Icon, {
|
|
3488
4250
|
name: "general.cog",
|
|
3489
4251
|
color: "foregroundSecondary"
|
|
3490
4252
|
});
|
|
3491
|
-
} else {
|
|
3492
|
-
return /*#__PURE__*/React.createElement(tapestryReact.StackView, {
|
|
3493
|
-
width: 2,
|
|
3494
|
-
height: 2
|
|
3495
|
-
}, /*#__PURE__*/React.createElement(tapestryReact.Logo, {
|
|
3496
|
-
name: currentAppName.toLowerCase().replace(/[\s]/, "-"),
|
|
3497
|
-
size: "sm",
|
|
3498
|
-
theme: "color",
|
|
3499
|
-
markOnly: true
|
|
3500
|
-
}));
|
|
3501
4253
|
}
|
|
4254
|
+
if (!VALID_LOGO_NAMES.has(logoName)) return null;
|
|
4255
|
+
return /*#__PURE__*/React.createElement(tapestryReact.StackView, {
|
|
4256
|
+
width: 2,
|
|
4257
|
+
height: 2
|
|
4258
|
+
}, /*#__PURE__*/React.createElement(tapestryReact.Logo, {
|
|
4259
|
+
name: logoName,
|
|
4260
|
+
size: "sm",
|
|
4261
|
+
theme: "color",
|
|
4262
|
+
markOnly: true
|
|
4263
|
+
}));
|
|
3502
4264
|
};
|
|
3503
4265
|
const HitResult = ({
|
|
3504
4266
|
hit,
|
|
@@ -3513,11 +4275,10 @@
|
|
|
3513
4275
|
color: "neutralDarker",
|
|
3514
4276
|
size: 4,
|
|
3515
4277
|
whiteSpace: "normal"
|
|
3516
|
-
}, hit.
|
|
4278
|
+
}, hitTitle(hit.hierarchy)));
|
|
3517
4279
|
};
|
|
3518
4280
|
const CustomSearchBox = ({
|
|
3519
|
-
currentAppName
|
|
3520
|
-
indexName
|
|
4281
|
+
currentAppName
|
|
3521
4282
|
}) => {
|
|
3522
4283
|
const {
|
|
3523
4284
|
query,
|
|
@@ -3528,7 +4289,10 @@
|
|
|
3528
4289
|
results
|
|
3529
4290
|
} = reactInstantsearchCore.useHits();
|
|
3530
4291
|
const groups = results === null || results === void 0 ? void 0 : results.hits.reduce((acc, hit) => {
|
|
3531
|
-
|
|
4292
|
+
// A result with no url can't be opened, so don't render it at all.
|
|
4293
|
+
if (!hit.url) return acc;
|
|
4294
|
+
// Fall back so a record without a product never yields an "undefined" heading.
|
|
4295
|
+
const key = hit.product || currentAppName || "Other";
|
|
3532
4296
|
if (!acc[key]) {
|
|
3533
4297
|
acc[key] = [];
|
|
3534
4298
|
}
|
|
@@ -3550,7 +4314,7 @@
|
|
|
3550
4314
|
name: "general.search"
|
|
3551
4315
|
}),
|
|
3552
4316
|
onChange: e => setQuery(e.target.value),
|
|
3553
|
-
onSelect: selectedValue => handleSearchSelect(selectedValue
|
|
4317
|
+
onSelect: selectedValue => handleSearchSelect(selectedValue),
|
|
3554
4318
|
placement: "bottom-start"
|
|
3555
4319
|
}, !hits.length && (/*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(tapestryReact.StackView, {
|
|
3556
4320
|
axis: "horizontal",
|
|
@@ -3562,7 +4326,7 @@
|
|
|
3562
4326
|
size: 4,
|
|
3563
4327
|
whiteSpace: "normal"
|
|
3564
4328
|
}, "No results match your search")))), groups && Object.keys(groups).map(key => {
|
|
3565
|
-
const
|
|
4329
|
+
const groupHits = groups[key];
|
|
3566
4330
|
return /*#__PURE__*/React.createElement(React.Fragment, {
|
|
3567
4331
|
key: key
|
|
3568
4332
|
}, resultIndex > 0 && /*#__PURE__*/React.createElement(tapestryReact.Divider, {
|
|
@@ -3575,12 +4339,12 @@
|
|
|
3575
4339
|
paddingVertical: 1
|
|
3576
4340
|
}, /*#__PURE__*/React.createElement(HeadingIcon, {
|
|
3577
4341
|
currentAppName: currentAppName,
|
|
3578
|
-
|
|
4342
|
+
product: key
|
|
3579
4343
|
}), /*#__PURE__*/React.createElement(tapestryReact.Text, {
|
|
3580
4344
|
size: 5,
|
|
3581
4345
|
color: "foregroundSecondary",
|
|
3582
4346
|
fontWeight: 500
|
|
3583
|
-
}, key)),
|
|
4347
|
+
}, key)), groupHits.map(hit => (/*#__PURE__*/React.createElement(HitResult, {
|
|
3584
4348
|
key: hit.objectID,
|
|
3585
4349
|
hit: hit,
|
|
3586
4350
|
index: resultIndex++
|
|
@@ -3588,8 +4352,8 @@
|
|
|
3588
4352
|
})));
|
|
3589
4353
|
};
|
|
3590
4354
|
|
|
4355
|
+
const HELP_CENTER_INDEX = "help_center_english";
|
|
3591
4356
|
const DocumentationSearch = ({
|
|
3592
|
-
indexName,
|
|
3593
4357
|
currentAppName
|
|
3594
4358
|
}) => {
|
|
3595
4359
|
const {
|
|
@@ -3600,13 +4364,13 @@
|
|
|
3600
4364
|
} = useStore();
|
|
3601
4365
|
const searchClient = algoliasearch(algoliaApplicationId, algoliaSearchOnlyApiKey);
|
|
3602
4366
|
return /*#__PURE__*/React.createElement(reactInstantsearchCore.InstantSearch, {
|
|
3603
|
-
indexName:
|
|
4367
|
+
indexName: HELP_CENTER_INDEX,
|
|
3604
4368
|
searchClient: searchClient
|
|
3605
4369
|
}, /*#__PURE__*/React.createElement(reactInstantsearchCore.Configure, {
|
|
3606
|
-
hitsPerPage: 5
|
|
4370
|
+
hitsPerPage: 5,
|
|
4371
|
+
optionalFilters: currentAppName ? [`product:${currentAppName}`] : undefined
|
|
3607
4372
|
}), /*#__PURE__*/React.createElement(CustomSearchBox, {
|
|
3608
|
-
currentAppName: currentAppName
|
|
3609
|
-
indexName: indexName
|
|
4373
|
+
currentAppName: currentAppName
|
|
3610
4374
|
}));
|
|
3611
4375
|
};
|
|
3612
4376
|
|
|
@@ -3614,6 +4378,19 @@
|
|
|
3614
4378
|
LoadingIndicator: LoadingIndicator$1,
|
|
3615
4379
|
StackView: StackView$1
|
|
3616
4380
|
} = addOns.components;
|
|
4381
|
+
function CalendlyWidget({
|
|
4382
|
+
url,
|
|
4383
|
+
pageSettings,
|
|
4384
|
+
prefill,
|
|
4385
|
+
styles
|
|
4386
|
+
}) {
|
|
4387
|
+
return url ? (/*#__PURE__*/React.createElement(reactCalendly.InlineWidget, {
|
|
4388
|
+
url: url,
|
|
4389
|
+
pageSettings: pageSettings,
|
|
4390
|
+
prefill: prefill,
|
|
4391
|
+
styles: styles
|
|
4392
|
+
})) : null;
|
|
4393
|
+
}
|
|
3617
4394
|
function HelpdeskComponent({
|
|
3618
4395
|
appName,
|
|
3619
4396
|
closeDrawer,
|
|
@@ -3682,14 +4459,65 @@
|
|
|
3682
4459
|
spacing: 1
|
|
3683
4460
|
}, /*#__PURE__*/React.createElement(DrawerHeading, null, "Help and support"), /*#__PURE__*/React.createElement(LoadingIndicator$1, null)))),
|
|
3684
4461
|
additionalComponents: {
|
|
3685
|
-
|
|
3686
|
-
|
|
3687
|
-
|
|
3688
|
-
|
|
4462
|
+
CalendlyWidget: {
|
|
4463
|
+
component: CalendlyWidget,
|
|
4464
|
+
properties: {
|
|
4465
|
+
pageSettings: {
|
|
4466
|
+
attribute: false
|
|
4467
|
+
},
|
|
4468
|
+
prefill: {
|
|
4469
|
+
attribute: false
|
|
4470
|
+
},
|
|
4471
|
+
styles: {
|
|
4472
|
+
attribute: false
|
|
4473
|
+
},
|
|
4474
|
+
url: {
|
|
4475
|
+
type: String
|
|
4476
|
+
}
|
|
4477
|
+
}
|
|
4478
|
+
},
|
|
4479
|
+
DocumentationSearch: {
|
|
4480
|
+
component: DocumentationSearch,
|
|
4481
|
+
properties: {
|
|
4482
|
+
currentAppName: {
|
|
4483
|
+
type: String
|
|
4484
|
+
}
|
|
4485
|
+
}
|
|
4486
|
+
},
|
|
4487
|
+
DrawerHeader: {
|
|
4488
|
+
component: DrawerHeader,
|
|
4489
|
+
properties: {
|
|
4490
|
+
closeDrawer: {
|
|
4491
|
+
event: false
|
|
4492
|
+
}
|
|
4493
|
+
}
|
|
4494
|
+
},
|
|
3689
4495
|
DrawerHeading,
|
|
3690
|
-
PicobotIframe
|
|
4496
|
+
PicobotIframe: {
|
|
4497
|
+
component: PicobotIframe,
|
|
4498
|
+
properties: {
|
|
4499
|
+
currentPath: {
|
|
4500
|
+
attribute: false
|
|
4501
|
+
},
|
|
4502
|
+
onChangePath: {
|
|
4503
|
+
event: false,
|
|
4504
|
+
type: Function
|
|
4505
|
+
},
|
|
4506
|
+
onMessage: {
|
|
4507
|
+
event: false,
|
|
4508
|
+
type: Function
|
|
4509
|
+
}
|
|
4510
|
+
}
|
|
4511
|
+
},
|
|
3691
4512
|
ScrollContainer,
|
|
3692
|
-
SwitchView
|
|
4513
|
+
SwitchView: {
|
|
4514
|
+
component: SwitchView,
|
|
4515
|
+
properties: {
|
|
4516
|
+
activeIndex: {
|
|
4517
|
+
type: Number
|
|
4518
|
+
}
|
|
4519
|
+
}
|
|
4520
|
+
}
|
|
3693
4521
|
},
|
|
3694
4522
|
args: {
|
|
3695
4523
|
appName,
|
|
@@ -4293,8 +5121,13 @@
|
|
|
4293
5121
|
function NavLinkWithBadge({
|
|
4294
5122
|
children,
|
|
4295
5123
|
badge,
|
|
4296
|
-
|
|
4297
|
-
|
|
5124
|
+
borderBottomColor,
|
|
5125
|
+
borderBottomStyle,
|
|
5126
|
+
borderBottomWidth,
|
|
5127
|
+
color,
|
|
5128
|
+
fontSize,
|
|
5129
|
+
fontWeight,
|
|
5130
|
+
onClick
|
|
4298
5131
|
}) {
|
|
4299
5132
|
return /*#__PURE__*/React.createElement(tapestryReact.StackView, {
|
|
4300
5133
|
as: "button",
|
|
@@ -4304,7 +5137,14 @@
|
|
|
4304
5137
|
paddingBottom: 0.75,
|
|
4305
5138
|
onClick: () => onClick(),
|
|
4306
5139
|
spacing: 1,
|
|
4307
|
-
...
|
|
5140
|
+
...{
|
|
5141
|
+
borderBottomColor,
|
|
5142
|
+
borderBottomStyle,
|
|
5143
|
+
borderBottomWidth,
|
|
5144
|
+
color,
|
|
5145
|
+
fontSize,
|
|
5146
|
+
fontWeight
|
|
5147
|
+
}
|
|
4308
5148
|
}, children, badge && /*#__PURE__*/React.createElement(tapestryReact.Badge, {
|
|
4309
5149
|
size: "sm",
|
|
4310
5150
|
fontWeight: "600",
|
|
@@ -4356,11 +5196,55 @@
|
|
|
4356
5196
|
spacing: 1
|
|
4357
5197
|
}, /*#__PURE__*/React.createElement(DrawerHeading, null, "To do"), /*#__PURE__*/React.createElement(LoadingIndicator, null)))),
|
|
4358
5198
|
additionalComponents: {
|
|
4359
|
-
DrawerHeader
|
|
5199
|
+
DrawerHeader: {
|
|
5200
|
+
component: DrawerHeader,
|
|
5201
|
+
properties: {
|
|
5202
|
+
closeDrawer: {
|
|
5203
|
+
event: false,
|
|
5204
|
+
type: Function
|
|
5205
|
+
}
|
|
5206
|
+
}
|
|
5207
|
+
},
|
|
4360
5208
|
DrawerHeading,
|
|
4361
|
-
NavLinkWithBadge
|
|
5209
|
+
NavLinkWithBadge: {
|
|
5210
|
+
component: NavLinkWithBadge,
|
|
5211
|
+
properties: {
|
|
5212
|
+
badge: {
|
|
5213
|
+
attribute: false
|
|
5214
|
+
},
|
|
5215
|
+
borderBottomColor: {
|
|
5216
|
+
type: String
|
|
5217
|
+
},
|
|
5218
|
+
borderBottomStyle: {
|
|
5219
|
+
type: String
|
|
5220
|
+
},
|
|
5221
|
+
borderBottomWidth: {
|
|
5222
|
+
type: String
|
|
5223
|
+
},
|
|
5224
|
+
color: {
|
|
5225
|
+
type: String
|
|
5226
|
+
},
|
|
5227
|
+
fontSize: {
|
|
5228
|
+
attribute: false
|
|
5229
|
+
},
|
|
5230
|
+
fontWeight: {
|
|
5231
|
+
attribute: false
|
|
5232
|
+
},
|
|
5233
|
+
onClick: {
|
|
5234
|
+
event: false,
|
|
5235
|
+
type: Function
|
|
5236
|
+
}
|
|
5237
|
+
}
|
|
5238
|
+
},
|
|
4362
5239
|
ScrollContainer,
|
|
4363
|
-
SwitchView
|
|
5240
|
+
SwitchView: {
|
|
5241
|
+
component: SwitchView,
|
|
5242
|
+
properties: {
|
|
5243
|
+
activeIndex: {
|
|
5244
|
+
type: Number
|
|
5245
|
+
}
|
|
5246
|
+
}
|
|
5247
|
+
}
|
|
4364
5248
|
},
|
|
4365
5249
|
args: {
|
|
4366
5250
|
closeDrawer,
|