@planningcenter/topbar 11.2.0 → 11.2.1-rc.1

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.
@@ -1,7 +1,7 @@
1
1
  import { Link } from '@planningcenter/tapestry';
2
2
  import * as React from 'react';
3
3
  import React__default, { useState, useEffect, useMemo, useLayoutEffect, useCallback, useRef } from 'react';
4
- import pcoUrl from '@planningcenter/url';
4
+ import pcoUrl$1 from '@planningcenter/url';
5
5
  import * as general from '@planningcenter/icons/paths/general';
6
6
  import Jolt from '@planningcenter/jolt-client';
7
7
  import { useSyncExternalStore } from 'use-sync-external-store/shim';
@@ -450,7 +450,7 @@ class Provider extends React__default.Component {
450
450
  announcements: newAnnouncements
451
451
  };
452
452
  });
453
- return apiRequest(this.props.configuredFetch || defaultFetch, `${pcoUrl(this.props.env)("api")}/people/v2/me/platform_notifications/${id}/dismiss`, {
453
+ return apiRequest(this.props.configuredFetch || defaultFetch, `${pcoUrl$1(this.props.env)("api")}/people/v2/me/platform_notifications/${id}/dismiss`, {
454
454
  method: "POST"
455
455
  });
456
456
  }
@@ -695,7 +695,7 @@ const useJoltChannelSubscription = ({
695
695
  const channel = joltIsV2(jolt) ? buildJoltChannel(jolt, channelName, fetchSubscribeTokenFn) : buildLegacyJoltChannel(jolt, channelName, fetchSubscribeTokenFn);
696
696
  setJoltChannel(channel);
697
697
  setJoltInstance(jolt);
698
- }, [apiRequest, channelName, env, pcoUrl, railsCsrfFetch]);
698
+ }, [apiRequest, channelName, env, pcoUrl$1, railsCsrfFetch]);
699
699
  React__default.useEffect(() => {
700
700
  if (enabled) {
701
701
  subscribeToJoltChannel();
@@ -900,7 +900,7 @@ const useTopbarAPI = ({
900
900
  fetch();
901
901
  }, []);
902
902
  const fetch = () => {
903
- apiRequest(configuredFetch, `${pcoUrl(env)("api")}/people/v2/me/topbar`).then(({
903
+ apiRequest(configuredFetch, `${pcoUrl$1(env)("api")}/people/v2/me/topbar`).then(({
904
904
  json,
905
905
  ok
906
906
  }) => {
@@ -1047,7 +1047,7 @@ function setConnectedPeopleInStore(connectedPeople) {
1047
1047
  });
1048
1048
  }
1049
1049
  function switchConnectedPerson(env, toId, returnPath) {
1050
- apiRequest(railsCsrfFetch, `${pcoUrl(env)("api")}/login/v2/me/switch_connected_person`, {
1050
+ apiRequest(railsCsrfFetch, `${pcoUrl$1(env)("api")}/login/v2/me/switch_connected_person`, {
1051
1051
  data: {
1052
1052
  data: {
1053
1053
  attributes: {
@@ -1065,7 +1065,7 @@ function switchConnectedPerson(env, toId, returnPath) {
1065
1065
  });
1066
1066
  }
1067
1067
  function unlinkConnectedPeople(env) {
1068
- apiRequest(railsCsrfFetch, `${pcoUrl(env)("api")}/login/v2/me/unlink`, {
1068
+ apiRequest(railsCsrfFetch, `${pcoUrl$1(env)("api")}/login/v2/me/unlink`, {
1069
1069
  method: "POST"
1070
1070
  }).then(({
1071
1071
  json
@@ -1082,7 +1082,7 @@ class ConnectedPeopleProvider extends React__default.Component {
1082
1082
  };
1083
1083
  }
1084
1084
  fetch() {
1085
- apiRequest(this.props.configuredFetch, `${pcoUrl(this.props.env)("api")}/login/v2/me/connected_people`).then(({
1085
+ apiRequest(this.props.configuredFetch, `${pcoUrl$1(this.props.env)("api")}/login/v2/me/connected_people`).then(({
1086
1086
  json,
1087
1087
  ok
1088
1088
  }) => {
@@ -2141,7 +2141,7 @@ const AppsMenu = ({
2141
2141
  component: "a",
2142
2142
  "data-pendo": `app-switcher--change-app--${homeProduct.name}`,
2143
2143
  "data-turbolinks": false,
2144
- href: appName === homeProduct.name ? `${pcoUrl(env)("accounts")}/apps/${appName}` : pcoUrl(env)("accounts") + getAppsSwitchProductPath(homeProduct.name),
2144
+ href: appName === homeProduct.name ? `${pcoUrl$1(env)("accounts")}/apps/${appName}` : pcoUrl$1(env)("accounts") + getAppsSwitchProductPath(homeProduct.name),
2145
2145
  style: {
2146
2146
  ...listItemProps,
2147
2147
  borderBottom: "1px solid var(--t-border-color)"
@@ -2167,7 +2167,7 @@ const AppsMenu = ({
2167
2167
  component: "a",
2168
2168
  "data-pendo": `app-switcher--change-app--${name}`,
2169
2169
  "data-turbolinks": false,
2170
- href: name === appName ? `${pcoUrl(env)("accounts")}/apps/${name.toLowerCase()}` : pcoUrl(env)("accounts") + getAppsSwitchProductPath(name),
2170
+ href: name === appName ? `${pcoUrl$1(env)("accounts")}/apps/${name.toLowerCase()}` : pcoUrl$1(env)("accounts") + getAppsSwitchProductPath(name),
2171
2171
  key: name,
2172
2172
  style: {
2173
2173
  ...listItemProps,
@@ -2192,7 +2192,7 @@ const AppsMenu = ({
2192
2192
  component: "a",
2193
2193
  "data-pendo": `app-switcher--change-app--${accountsProduct.name}`,
2194
2194
  "data-turbolinks": false,
2195
- href: pcoUrl(env)("accounts") + getAppsSwitchProductPath(accountsProduct.name),
2195
+ href: pcoUrl$1(env)("accounts") + getAppsSwitchProductPath(accountsProduct.name),
2196
2196
  style: {
2197
2197
  ...listItemProps,
2198
2198
  borderTop: "1px solid var(--t-border-color)",
@@ -2299,7 +2299,7 @@ const UserMenu = ({
2299
2299
  setUserMenuVisible,
2300
2300
  userMenuVisible
2301
2301
  }) => {
2302
- const logoutUrl = `${pcoUrl(env)("login")}/logout`;
2302
+ const logoutUrl = `${pcoUrl$1(env)("login")}/logout`;
2303
2303
  return /*#__PURE__*/React__default.createElement(PopupRoot, null, /*#__PURE__*/React__default.createElement(Popup, {
2304
2304
  component: Outsider,
2305
2305
  onOutsideClick: () => setUserMenuVisible(false),
@@ -2572,7 +2572,7 @@ function Avatar({
2572
2572
  style,
2573
2573
  ...nativeProps
2574
2574
  }) {
2575
- const url = incomingUrl || `${pcoUrl(env)("people")}/static/no_photo_thumbnail_gray.svg`;
2575
+ const url = incomingUrl || `${pcoUrl$1(env)("people")}/static/no_photo_thumbnail_gray.svg`;
2576
2576
  return /*#__PURE__*/React__default.createElement("div", {
2577
2577
  className: styles$4.avatar,
2578
2578
  style: {
@@ -2606,7 +2606,7 @@ function AccountSettingsLink({
2606
2606
  className: classnames(styles$3["appmenu-list-a"], styles$3["appmenu-list-a--bottom"]),
2607
2607
  "data-pendo": `app-switcher--change-app--${accountsName}`,
2608
2608
  "data-turbolinks": false,
2609
- href: pcoUrl(env)(accountsName) + getAppsSwitchProductPath(accountsName)
2609
+ href: pcoUrl$1(env)(accountsName) + getAppsSwitchProductPath(accountsName)
2610
2610
  }, /*#__PURE__*/React__default.createElement("svg", {
2611
2611
  "aria-hidden": "true",
2612
2612
  height: 24,
@@ -2748,7 +2748,7 @@ const SmallTopbar = ({
2748
2748
  "data-pendo": `app-switcher--change-app--${homeProduct === null || homeProduct === void 0 ? void 0 : homeProduct.name}`,
2749
2749
  "data-turbolinks": false,
2750
2750
  className: classnames(styles$3["appmenu-list-a"], styles$3["appmenu-list-a--top"]),
2751
- href: homeProduct ? pcoUrl(props.env)("accounts") + getAppsSwitchProductPath(homeProduct === null || homeProduct === void 0 ? void 0 : homeProduct.name) : undefined
2751
+ href: homeProduct ? pcoUrl$1(props.env)("accounts") + getAppsSwitchProductPath(homeProduct === null || homeProduct === void 0 ? void 0 : homeProduct.name) : undefined
2752
2752
  }, /*#__PURE__*/React__default.createElement(UserMenuAppLockup, {
2753
2753
  appName: homeProduct ? homeProduct.name : props.appName
2754
2754
  })), /*#__PURE__*/React__default.createElement("div", {
@@ -2767,7 +2767,7 @@ const SmallTopbar = ({
2767
2767
  "data-pendo": `app-switcher--change-app--${name}`,
2768
2768
  "data-turbolinks": false,
2769
2769
  className: styles$3["appmenu-list-a"],
2770
- href: pcoUrl(props.env)("accounts") + getAppsSwitchProductPath(name)
2770
+ href: pcoUrl$1(props.env)("accounts") + getAppsSwitchProductPath(name)
2771
2771
  }, /*#__PURE__*/React__default.createElement(UserMenuAppLockup, {
2772
2772
  appName: name
2773
2773
  }))))), accountsProduct && (/*#__PURE__*/React__default.createElement("li", {
@@ -2812,7 +2812,7 @@ function getLoginProfilePath({
2812
2812
  userId
2813
2813
  }) {
2814
2814
  var _a, _b;
2815
- const basePath = pcoUrl(env)("login") + `/people/AC${userId}`;
2815
+ const basePath = pcoUrl$1(env)("login") + `/people/AC${userId}`;
2816
2816
  const pathname = (_b = (_a = window === null || window === void 0 ? void 0 : window.location) === null || _a === void 0 ? void 0 : _a.pathname) === null || _b === void 0 ? void 0 : _b.toString();
2817
2817
  if (!pathname) return basePath;
2818
2818
  const params = new URLSearchParams();
@@ -2887,7 +2887,7 @@ function useFragmentRouting() {
2887
2887
  const key = "toolbar";
2888
2888
  const [toolbarFragment, updateFragment] = useFragmentKey(key);
2889
2889
  const onChangeRoute = useCallback((tool, path = "") => {
2890
- const nextHash = `#${key}=${tool}/${path}`;
2890
+ const nextHash = `#${key}=${tool}/${encodeQueryString(path)}`;
2891
2891
  history.replaceState(undefined, "", nextHash);
2892
2892
  updateFragment(nextHash);
2893
2893
  }, [updateFragment]);
@@ -2895,7 +2895,7 @@ function useFragmentRouting() {
2895
2895
  if (toolbarFragment) {
2896
2896
  const [tool, ...rest] = toolbarFragment.split("/");
2897
2897
  return {
2898
- path: rest.join("/"),
2898
+ path: decodeQueryString(rest.join("/")),
2899
2899
  tool
2900
2900
  };
2901
2901
  } else {
@@ -2908,6 +2908,18 @@ function useFragmentRouting() {
2908
2908
  }, []);
2909
2909
  return [route, onChangeRoute, clearRoute];
2910
2910
  }
2911
+ // The fragment value is parsed with /toolbar=([^&]+)/, so any "&" in a
2912
+ // tool-reported path would silently truncate the route. We encode just the
2913
+ // query portion on the way into the hash and decode on the way out, so
2914
+ // callers can pass and receive raw paths.
2915
+ function encodeQueryString(path) {
2916
+ const [pathPart, queryPart] = path.split("?", 2);
2917
+ return queryPart ? `${pathPart}?${encodeURIComponent(queryPart)}` : pathPart;
2918
+ }
2919
+ function decodeQueryString(path) {
2920
+ const [pathPart, queryPart] = path.split("?", 2);
2921
+ return queryPart ? `${pathPart}?${decodeURIComponent(queryPart)}` : pathPart;
2922
+ }
2911
2923
 
2912
2924
  const {
2913
2925
  generateColorScale
@@ -3428,6 +3440,16 @@ const useToolbarReducer = overrides => React.useReducer(reducer, {
3428
3440
 
3429
3441
  var styles = {"toolbar":"toolbar-KHJR5","toolbar-button":"toolbar-button-Ye4h5","is-current":"is-current-DZK47","toolbar-button--avatar":"toolbar-button--avatar-5FUik","toolbar-button--home":"toolbar-button--home-Pthd0"};
3430
3442
 
3443
+ const pcoUrl = pcoUrl$1;
3444
+
3445
+ function buildPicobotSrc(origin, path) {
3446
+ const [rawPath, queryPart] = path.replace(/^\/+/, "").split("?", 2);
3447
+ const safePath = rawPath.split("/").filter(segment => segment && segment !== "." && segment !== "..").join("/");
3448
+ const pathWithPrefix = ["topbar", safePath].filter(Boolean).join("/");
3449
+ const relativePath = queryPart !== undefined ? `${pathWithPrefix}?${queryPart}` : pathWithPrefix;
3450
+ return new URL(relativePath, origin).toString();
3451
+ }
3452
+
3431
3453
  const handleSearchSelect = (hit, indexName) => {
3432
3454
  const appName = indexName.split("_")[1];
3433
3455
  const zendeskUrl = `https://${appName}.zendesk.com/hc/${hit.locale.locale}/articles/${hit.id}`;
@@ -3580,9 +3602,62 @@ const {
3580
3602
  function HelpdeskComponent({
3581
3603
  appName,
3582
3604
  closeDrawer,
3605
+ currentPath,
3583
3606
  env,
3584
- helpdeskConfig
3607
+ helpdeskConfig,
3608
+ onChangePath
3585
3609
  }) {
3610
+ const PicobotIframe = React__default.useMemo(() => {
3611
+ const picobotOrigin = pcoUrl(env)("picobot");
3612
+ return function PicobotIframe({
3613
+ currentPath: iframePath,
3614
+ onChangePath: onIframeChangePath,
3615
+ onMessage
3616
+ }) {
3617
+ const ref = React__default.useRef(null);
3618
+ const initialPath = iframePath || "";
3619
+ // Tracks the path picobot last reported via postMessage so an
3620
+ // incoming currentPath that matches it doesn't trigger a reload.
3621
+ // Picobot sends a bare relative path (e.g. "conversations/abc") with
3622
+ // no leading slash or /topbar prefix—buildPicobotSrc adds those.
3623
+ const lastReportedPathRef = React__default.useRef(initialPath);
3624
+ const [loadedPath, setLoadedPath] = React__default.useState(initialPath);
3625
+ React__default.useEffect(() => {
3626
+ if (!onIframeChangePath && !onMessage) return undefined;
3627
+ const handler = e => {
3628
+ var _a, _b;
3629
+ if (e.origin !== picobotOrigin) return;
3630
+ if (!e.source || !ref.current) return;
3631
+ if (e.source !== ref.current.contentWindow) return;
3632
+ if (((_a = e.data) === null || _a === void 0 ? void 0 : _a.type) === "NEW_ROUTE_RENDERED" && onIframeChangePath) {
3633
+ const reportedPath = String((_b = e.data.payload) !== null && _b !== void 0 ? _b : "");
3634
+ lastReportedPathRef.current = reportedPath;
3635
+ onIframeChangePath(reportedPath);
3636
+ }
3637
+ if (onMessage) onMessage(e.data);
3638
+ };
3639
+ window.addEventListener("message", handler);
3640
+ return () => window.removeEventListener("message", handler);
3641
+ }, [onIframeChangePath, onMessage]);
3642
+ React__default.useEffect(() => {
3643
+ const incomingPath = iframePath || "";
3644
+ if (lastReportedPathRef.current === incomingPath) return;
3645
+ setLoadedPath(incomingPath);
3646
+ }, [iframePath]);
3647
+ const iframeSrc = buildPicobotSrc(picobotOrigin, loadedPath);
3648
+ return /*#__PURE__*/React__default.createElement("iframe", {
3649
+ ref: ref,
3650
+ src: iframeSrc,
3651
+ style: {
3652
+ border: 0,
3653
+ flex: 1,
3654
+ height: "100%",
3655
+ width: "100%"
3656
+ },
3657
+ title: "Picobot"
3658
+ });
3659
+ };
3660
+ }, [env]);
3586
3661
  return /*#__PURE__*/React__default.createElement(UiExtensionFirstPartyComponent, {
3587
3662
  grow: 1,
3588
3663
  display: "flex",
@@ -3602,6 +3677,7 @@ function HelpdeskComponent({
3602
3677
  DocumentationSearch,
3603
3678
  DrawerHeader,
3604
3679
  DrawerHeading,
3680
+ PicobotIframe,
3605
3681
  ScrollContainer,
3606
3682
  SwitchView
3607
3683
  },
@@ -3609,7 +3685,9 @@ function HelpdeskComponent({
3609
3685
  appName,
3610
3686
  closeDrawer,
3611
3687
  currentPageParams: location.search,
3612
- helpdeskConfig
3688
+ currentPath,
3689
+ helpdeskConfig,
3690
+ onChangePath
3613
3691
  }
3614
3692
  });
3615
3693
  }
@@ -3918,7 +3996,7 @@ const useNotifications = ({
3918
3996
  id,
3919
3997
  type: "graph_notification"
3920
3998
  }], []);
3921
- return apiRequest(railsCsrfFetch, `${pcoUrl(env)("api")}/notifications/v2/me/bulk_update`, {
3999
+ return apiRequest(railsCsrfFetch, `${pcoUrl$1(env)("api")}/notifications/v2/me/bulk_update`, {
3922
4000
  data: {
3923
4001
  data: {
3924
4002
  attributes: {},
@@ -3948,7 +4026,7 @@ const useNotifications = ({
3948
4026
  });
3949
4027
  };
3950
4028
  const handleCreateTask = useCallback(async notification => {
3951
- return apiRequest(railsCsrfFetch, `${pcoUrl(env)("api")}/home/v2/me/task_lists/inbox/tasks`, {
4029
+ return apiRequest(railsCsrfFetch, `${pcoUrl$1(env)("api")}/home/v2/me/task_lists/inbox/tasks`, {
3952
4030
  data: {
3953
4031
  data: {
3954
4032
  attributes: {
@@ -4404,38 +4482,29 @@ function useStableIframeSrcAttributeAndSyncFragmentRoute({
4404
4482
  setFragmentRoute,
4405
4483
  env
4406
4484
  }) {
4407
- const unencodedCurrentPath = decodeQueryString(currentPath || "");
4408
- const [iframeSrcPath, setIframeSrcPath] = useState(unencodedCurrentPath);
4409
- const internalPathOfChatRef = useRef(unencodedCurrentPath);
4410
- const chatBaseURL = pcoUrl(env)("chat");
4485
+ const initialPath = currentPath || "";
4486
+ const [iframeSrcPath, setIframeSrcPath] = useState(initialPath);
4487
+ const internalPathOfChatRef = useRef(initialPath);
4488
+ const chatBaseURL = pcoUrl$1(env)("chat");
4411
4489
  const updateFragmentRouteAndInternalPath = useCallback(event => {
4412
- const unencodedPath = event.data.payload.replace(/^\/?topbar/, "") // Maybe this should happen on the child side of things?
4490
+ const reportedPath = event.data.payload.replace(/^\/?topbar/, "") // Maybe this should happen on the child side of things?
4413
4491
  .replace(/^\/+/, "");
4414
- internalPathOfChatRef.current = unencodedPath;
4415
- // URL encode the payload's query string for the fragment route
4416
- // so possible query params don't get dropped by the fragment router
4417
- setFragmentRoute(encodeQueryString(unencodedPath));
4492
+ internalPathOfChatRef.current = reportedPath;
4493
+ setFragmentRoute(reportedPath);
4418
4494
  }, [setFragmentRoute]);
4419
4495
  useHandleMessageFromChildFrame(updateFragmentRouteAndInternalPath, {
4420
4496
  allowedOrigin: chatBaseURL,
4421
4497
  messageType: "NEW_ROUTE_RENDERED"
4422
4498
  });
4423
4499
  useEffect(() => {
4424
- if (internalPathOfChatRef.current !== unencodedCurrentPath) {
4425
- setIframeSrcPath(unencodedCurrentPath);
4500
+ const incomingPath = currentPath || "";
4501
+ if (internalPathOfChatRef.current !== incomingPath) {
4502
+ setIframeSrcPath(incomingPath);
4426
4503
  }
4427
4504
  }, [currentPath]);
4428
4505
  const iframeSrc = [chatBaseURL, "topbar", iframeSrcPath].filter(Boolean).join("/");
4429
4506
  return iframeSrc;
4430
4507
  }
4431
- function encodeQueryString(path) {
4432
- const [pathPart, queryPart] = path.split("?", 2);
4433
- return queryPart ? `${pathPart}?${encodeURIComponent(queryPart)}` : pathPart;
4434
- }
4435
- function decodeQueryString(path) {
4436
- const [pathPart, queryPart] = path.split("?", 2);
4437
- return queryPart ? `${pathPart}?${decodeURIComponent(queryPart)}` : pathPart;
4438
- }
4439
4508
 
4440
4509
  function ChatTool({
4441
4510
  env,
@@ -4450,7 +4519,7 @@ function ChatTool({
4450
4519
  setFragmentRoute: onChangePath
4451
4520
  });
4452
4521
  useHandleMessageFromChildFrame(closeDrawer, {
4453
- allowedOrigin: pcoUrl(env)("chat"),
4522
+ allowedOrigin: pcoUrl$1(env)("chat"),
4454
4523
  messageType: "CLOSE_DRAWER"
4455
4524
  });
4456
4525
  if (currentPath === "blankstate") {
@@ -4561,6 +4630,7 @@ function isToolPathExemptFromStorage(toolName, path) {
4561
4630
  }
4562
4631
  const TOOLS_MAP = {
4563
4632
  Chat,
4633
+ Helpdesk,
4564
4634
  Notifications,
4565
4635
  Profile,
4566
4636
  Tasks
@@ -4643,7 +4713,7 @@ function Toolbar({
4643
4713
  env: env,
4644
4714
  userId: userId
4645
4715
  });
4646
- const logoutUrl = `${pcoUrl(env)("login")}/logout`;
4716
+ const logoutUrl = `${pcoUrl$1(env)("login")}/logout`;
4647
4717
  const profileUrl = `/people/AC${userId}`;
4648
4718
  const HelpDeskTool = Helpdesk;
4649
4719
  const setToolAndOpenDrawer = useCallback(content => {