@applicaster/zapp-react-native-utils 14.0.0-alpha.5974411329 → 14.0.0-alpha.6000342231

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.
Files changed (69) hide show
  1. package/actionsExecutor/ActionExecutorContext.tsx +0 -1
  2. package/actionsExecutor/ScreenActions.ts +20 -19
  3. package/analyticsUtils/AnalyticPlayerListener.ts +5 -2
  4. package/analyticsUtils/__tests__/analyticsUtils.test.js +0 -11
  5. package/analyticsUtils/playerAnalyticsTracker.ts +2 -1
  6. package/appUtils/accessibilityManager/const.ts +13 -0
  7. package/appUtils/accessibilityManager/hooks.ts +35 -1
  8. package/appUtils/accessibilityManager/index.ts +151 -30
  9. package/appUtils/accessibilityManager/utils.ts +24 -0
  10. package/appUtils/contextKeysManager/contextResolver.ts +12 -1
  11. package/appUtils/focusManager/__tests__/__snapshots__/focusManager.test.js.snap +8 -0
  12. package/appUtils/focusManager/__tests__/focusManager.test.js +1 -1
  13. package/appUtils/focusManager/events.ts +2 -0
  14. package/appUtils/focusManager/index.ios.ts +27 -0
  15. package/appUtils/focusManager/index.ts +86 -11
  16. package/appUtils/focusManagerAux/utils/index.ts +112 -3
  17. package/appUtils/focusManagerAux/utils/utils.ios.ts +35 -0
  18. package/appUtils/platform/platformUtils.ts +33 -3
  19. package/appUtils/playerManager/conts.ts +21 -0
  20. package/arrayUtils/__tests__/allTruthy.test.ts +24 -0
  21. package/arrayUtils/__tests__/anyThruthy.test.ts +24 -0
  22. package/arrayUtils/index.ts +5 -0
  23. package/configurationUtils/__tests__/manifestKeyParser.test.ts +0 -1
  24. package/configurationUtils/index.ts +1 -1
  25. package/focusManager/FocusManager.ts +78 -4
  26. package/focusManager/aux/index.ts +98 -0
  27. package/focusManager/utils.ts +12 -6
  28. package/index.d.ts +1 -1
  29. package/manifestUtils/defaultManifestConfigurations/player.js +188 -2
  30. package/manifestUtils/index.js +4 -0
  31. package/manifestUtils/keys.js +12 -0
  32. package/manifestUtils/sharedConfiguration/screenPicker/stylesFields.js +6 -0
  33. package/navigationUtils/index.ts +20 -17
  34. package/package.json +2 -2
  35. package/playerUtils/PlayerTTS/PlayerTTS.ts +359 -0
  36. package/playerUtils/PlayerTTS/index.ts +1 -0
  37. package/playerUtils/getPlayerActionButtons.ts +1 -1
  38. package/playerUtils/usePlayerTTS.ts +21 -0
  39. package/reactHooks/cell-click/__tests__/index.test.js +3 -0
  40. package/reactHooks/debugging/__tests__/index.test.js +0 -1
  41. package/reactHooks/feed/__tests__/useBatchLoading.test.tsx +8 -2
  42. package/reactHooks/feed/__tests__/useFeedLoader.test.tsx +57 -37
  43. package/reactHooks/feed/index.ts +2 -0
  44. package/reactHooks/feed/useBatchLoading.ts +14 -9
  45. package/reactHooks/feed/useFeedLoader.tsx +39 -59
  46. package/reactHooks/feed/useInflatedUrl.ts +23 -29
  47. package/reactHooks/feed/useLoadPipesDataDispatch.ts +63 -0
  48. package/reactHooks/layout/index.ts +1 -1
  49. package/reactHooks/navigation/useScreenStateStore.ts +3 -3
  50. package/reactHooks/state/index.ts +1 -1
  51. package/reactHooks/state/useHomeRiver.ts +4 -2
  52. package/screenPickerUtils/index.ts +13 -0
  53. package/storage/ScreenSingleValueProvider.ts +25 -22
  54. package/storage/ScreenStateMultiSelectProvider.ts +26 -23
  55. package/utils/__tests__/endsWith.test.ts +30 -0
  56. package/utils/__tests__/find.test.ts +36 -0
  57. package/utils/__tests__/omit.test.ts +19 -0
  58. package/utils/__tests__/path.test.ts +33 -0
  59. package/utils/__tests__/pathOr.test.ts +37 -0
  60. package/utils/__tests__/startsWith.test.ts +30 -0
  61. package/utils/__tests__/take.test.ts +40 -0
  62. package/utils/endsWith.ts +9 -0
  63. package/utils/find.ts +3 -0
  64. package/utils/index.ts +19 -1
  65. package/utils/omit.ts +5 -0
  66. package/utils/path.ts +5 -0
  67. package/utils/pathOr.ts +5 -0
  68. package/utils/startsWith.ts +9 -0
  69. package/utils/take.ts +5 -0
@@ -1,15 +1,12 @@
1
1
  import { complement, compose, isNil, map, min, prop, take, uniq } from "ramda";
2
2
  import * as React from "react";
3
- import {
4
- ZappPipes,
5
- useAppDispatch,
6
- useZappPipesFeed,
7
- } from "@applicaster/zapp-react-native-redux";
3
+ import { useZappPipesFeed } from "@applicaster/zapp-react-native-redux";
8
4
  import { isNilOrEmpty } from "../../reactUtils/helpers";
9
5
  import { ZappPipesSearchContext } from "@applicaster/zapp-react-native-ui-components/Contexts";
10
6
  import {
11
7
  getInflatedDataSourceUrl,
12
8
  getSearchContext,
9
+ useLoadPipesDataDispatch,
13
10
  } from "@applicaster/zapp-react-native-utils/reactHooks";
14
11
  import { isGallery } from "@applicaster/zapp-react-native-utils/componentsUtils";
15
12
  import { useScreenContext } from "../screen";
@@ -65,7 +62,6 @@ export const useBatchLoading = (
65
62
  componentsToRender: { data?: ZappDataSource; component_type: string }[],
66
63
  options: Options
67
64
  ) => {
68
- const dispatch = useAppDispatch();
69
65
  const { screen: screenContext, entry: entryContext } = useScreenContext();
70
66
  const [searchContext] = ZappPipesSearchContext.useZappPipesContext();
71
67
  const [hasEverBeenReady, setHasEverBeenReady] = React.useState(false);
@@ -122,6 +118,8 @@ export const useBatchLoading = (
122
118
 
123
119
  const feeds = useZappPipesFeed(feedUrls);
124
120
 
121
+ const loadPipesDataDispatcher = useLoadPipesDataDispatch();
122
+
125
123
  // dispatch loadPipesData for each feed that is not loaded
126
124
  const runBatchLoading = React.useCallback(() => {
127
125
  batchComponents.forEach((rawData: any) => {
@@ -140,13 +138,20 @@ export const useBatchLoading = (
140
138
 
141
139
  if (mappedFeedUrl) {
142
140
  // 4. load data
143
- return dispatch(
144
- ZappPipes.loadPipesData(mappedFeedUrl, { riverId: options.riverId })
141
+ return loadPipesDataDispatcher(
142
+ mappedFeedUrl,
143
+ {
144
+ riverId: options.riverId,
145
+ },
146
+ {
147
+ withResolvers: true,
148
+ withScreenRouteMapping: true,
149
+ }
145
150
  );
146
151
  }
147
152
  }
148
153
  });
149
- }, [feedUrls, feeds]);
154
+ }, [feedUrls, feeds, loadPipesDataDispatcher]);
150
155
 
151
156
  React.useEffect(() => {
152
157
  runBatchLoading();
@@ -1,16 +1,11 @@
1
1
  import React, { useEffect } from "react";
2
2
 
3
- import {
4
- ZappPipes,
5
- useAppDispatch,
6
- useZappPipesFeed,
7
- } from "@applicaster/zapp-react-native-redux";
3
+ import { useZappPipesFeed } from "@applicaster/zapp-react-native-redux";
8
4
 
9
5
  import { reactHooksLogger } from "../logger";
10
6
  import { shouldDispatchData, useIsInitialRender } from "../utils";
11
7
  import { useInflatedUrl } from "./useInflatedUrl";
12
- import { useRoute } from "../navigation";
13
- import { useScreenResolvers } from "@applicaster/zapp-react-native-utils/actionsExecutor/screenResolver";
8
+ import { useLoadPipesDataDispatch } from "./useLoadPipesDataDispatch";
14
9
 
15
10
  const logger = reactHooksLogger.addSubsystem("useFeedLoader");
16
11
 
@@ -42,74 +37,63 @@ export const useFeedLoader = ({
42
37
  mapping,
43
38
  pipesOptions = {},
44
39
  }: Props): FeedLoaderResponse => {
45
- useEffect(() => {
46
- if (!feedUrl) {
47
- logger.warning({
48
- message: "Required parameter feedUrl is missing",
49
- data: { feedUrl },
50
- });
51
- }
52
- }, []);
53
-
54
40
  const isInitialRender = useIsInitialRender();
55
- const dispatch = useAppDispatch();
56
- const { screenData } = useRoute();
57
- const resolvers = useScreenResolvers();
58
41
 
59
42
  const callableFeedUrl = useInflatedUrl({ feedUrl, mapping });
60
43
 
61
44
  const currentFeed = useZappPipesFeed(callableFeedUrl);
62
45
 
63
- const riverId =
64
- (screenData as LegacyNavigationScreenData)?.targetScreen?.id ??
65
- screenData?.id;
46
+ const loadPipesDataDispatcher = useLoadPipesDataDispatch();
66
47
 
67
48
  const reloadData = React.useCallback<ReloadDataFunction>(
68
49
  (silentRefresh = true, callback) => {
69
- if (callableFeedUrl) {
70
- dispatch(
71
- ZappPipes.loadPipesData(callableFeedUrl, {
72
- clearCache: true,
73
- silentRefresh,
74
- callback,
75
- riverId,
76
- resolvers,
77
- })
78
- );
79
- }
50
+ loadPipesDataDispatcher(
51
+ callableFeedUrl,
52
+ {
53
+ clearCache: true,
54
+ silentRefresh,
55
+ callback,
56
+ },
57
+ {
58
+ withResolvers: true,
59
+ }
60
+ );
80
61
  },
81
- [callableFeedUrl, resolvers]
62
+ [callableFeedUrl]
82
63
  );
83
64
 
84
65
  const loadNext: FeedLoaderResponse["loadNext"] = React.useCallback(() => {
85
66
  if (callableFeedUrl) {
86
67
  const nextFeed = currentFeed?.data?.next;
87
68
 
88
- if (nextFeed) {
89
- dispatch(
90
- ZappPipes.loadPipesData(nextFeed, {
91
- silentRefresh: true,
92
- parentFeed: callableFeedUrl,
93
- riverId,
94
- resolvers,
95
- })
96
- );
97
- }
69
+ loadPipesDataDispatcher(
70
+ nextFeed,
71
+ {
72
+ silentRefresh: true,
73
+ parentFeed: callableFeedUrl,
74
+ },
75
+ {
76
+ withResolvers: true,
77
+ }
78
+ );
98
79
  }
99
- }, [callableFeedUrl, currentFeed?.data?.next, resolvers]);
80
+ }, [callableFeedUrl, currentFeed?.data?.next]);
100
81
 
101
82
  useEffect(() => {
102
83
  if (
103
84
  shouldDispatchData(callableFeedUrl, currentFeed, pipesOptions.clearCache)
104
85
  ) {
105
86
  if (callableFeedUrl && !pipesOptions.skipLoading) {
106
- dispatch(
107
- ZappPipes.loadPipesData(callableFeedUrl, {
87
+ loadPipesDataDispatcher(
88
+ callableFeedUrl,
89
+ {
108
90
  ...pipesOptions,
109
91
  clearCache: true,
110
- riverId,
111
- resolvers,
112
- })
92
+ },
93
+ {
94
+ withResolvers: true,
95
+ withScreenRouteMapping: true,
96
+ }
113
97
  );
114
98
  } else if (!callableFeedUrl) {
115
99
  logger.info({
@@ -133,20 +117,16 @@ export const useFeedLoader = ({
133
117
  jsOnly: true,
134
118
  });
135
119
  }
136
- }, [resolvers]);
120
+ }, []);
137
121
 
138
122
  // Reload feed when feedUrl changes, unless skipLoading is true
139
123
  useEffect(() => {
140
124
  if (!isInitialRender && callableFeedUrl && !pipesOptions.skipLoading) {
141
- dispatch(
142
- ZappPipes.loadPipesData(callableFeedUrl, {
143
- ...pipesOptions,
144
- riverId,
145
- resolvers,
146
- })
147
- );
125
+ loadPipesDataDispatcher(callableFeedUrl, pipesOptions, {
126
+ withResolvers: true,
127
+ });
148
128
  }
149
- }, [callableFeedUrl, resolvers]);
129
+ }, [callableFeedUrl, isInitialRender, pipesOptions.skipLoading]);
150
130
 
151
131
  return React.useMemo(() => {
152
132
  if (!callableFeedUrl || !feedUrl) {
@@ -18,6 +18,7 @@ import {
18
18
  } from "@applicaster/zapp-pipes-v2-client";
19
19
  import { appStore } from "@applicaster/zapp-react-native-redux/AppStore";
20
20
  import { ENDPOINT_TAGS } from "../../types";
21
+ import { isNilOrEmpty } from "../../reactUtils/helpers";
21
22
 
22
23
  /**
23
24
  * will match any occurrence in a string of one or more word characters
@@ -75,15 +76,19 @@ export const getInflatedDataSourceUrl: GetInflatedDataSourceUrl = ({
75
76
  * https://foo.com/shows/A1234
76
77
  */
77
78
 
78
- if (!source) {
79
- // eslint-disable-next-line no-console
80
- console.error("source is empty", {
81
- source,
82
- contexts,
83
- mapping,
84
- });
79
+ if (!isNilOrEmpty(mapping)) {
80
+ if (!source) {
81
+ if (__DEV__) {
82
+ // eslint-disable-next-line no-console
83
+ throw new Error(
84
+ "getInflatedDataSourceUrl: source is empty while mapping is provided"
85
+ );
86
+ }
85
87
 
86
- return null;
88
+ return null;
89
+ }
90
+ } else {
91
+ return source || null;
87
92
  }
88
93
 
89
94
  // Hack because in tv we expect to get key names instead of values from the fake entry
@@ -193,28 +198,17 @@ export function useInflatedUrl({
193
198
 
194
199
  const url = useMemo(
195
200
  () =>
196
- mapping
197
- ? getInflatedDataSourceUrl({
198
- source: feedUrl,
199
- contexts: {
200
- entry: entryContext,
201
- screen: screenContext,
202
- search: getSearchContext(searchContext, mapping),
203
- },
204
- mapping,
205
- })
206
- : feedUrl,
207
- [feedUrl, mapping]
201
+ getInflatedDataSourceUrl({
202
+ source: feedUrl,
203
+ contexts: {
204
+ entry: entryContext,
205
+ screen: screenContext,
206
+ search: getSearchContext(searchContext, mapping),
207
+ },
208
+ mapping,
209
+ }),
210
+ [entryContext, feedUrl, mapping, screenContext, searchContext]
208
211
  );
209
212
 
210
- if (!feedUrl) {
211
- logger.warning({
212
- message: "Required parameter feedUrl is missing",
213
- data: { feedUrl },
214
- });
215
-
216
- return null;
217
- }
218
-
219
213
  return url;
220
214
  }
@@ -0,0 +1,63 @@
1
+ import {
2
+ useAppDispatch,
3
+ ZappPipes,
4
+ } from "@applicaster/zapp-react-native-redux";
5
+ import { applyScreenRouteDefaults } from "@applicaster/zapp-react-native-redux/ZappPipes/feedProcessor";
6
+ import React from "react";
7
+ import { useScreenResolvers } from "../../actionsExecutor/screenResolver";
8
+ import { useRoute } from "../navigation";
9
+ import { useScreenStateStore } from "../navigation/useScreenStateStore";
10
+
11
+ export const useLoadPipesDataDispatch = () => {
12
+ const screenStateStore = useScreenStateStore();
13
+ const resolvers = useScreenResolvers();
14
+ const dispatch = useAppDispatch();
15
+
16
+ const { pathname, screenData } = useRoute();
17
+
18
+ const riverId =
19
+ (screenData as LegacyNavigationScreenData)?.targetScreen?.id ??
20
+ screenData?.id;
21
+
22
+ const onLoadCB = (options) => (data, _err) => {
23
+ options?.callback?.();
24
+
25
+ if (data) {
26
+ applyScreenRouteDefaults(data, screenStateStore, pathname);
27
+ }
28
+ };
29
+
30
+ return React.useCallback(
31
+ (
32
+ url: string,
33
+ options: {
34
+ callback?: (data: unknown, error?: Error | null | undefined) => void;
35
+ riverId?: string;
36
+ clearCache?: boolean;
37
+ silentRefresh?: boolean;
38
+ parentFeed?: string;
39
+ } = {},
40
+ {
41
+ withResolvers = false,
42
+ withScreenRouteMapping = false,
43
+ }: {
44
+ withResolvers?: boolean;
45
+ withScreenRouteMapping?: boolean;
46
+ } = {}
47
+ ) => {
48
+ if (url) {
49
+ dispatch(
50
+ ZappPipes.loadPipesData(url, {
51
+ riverId,
52
+ resolvers: withResolvers ? resolvers : undefined,
53
+ ...options,
54
+ callback: withScreenRouteMapping
55
+ ? onLoadCB(options)
56
+ : options?.callback,
57
+ })
58
+ );
59
+ }
60
+ },
61
+ []
62
+ );
63
+ };
@@ -44,7 +44,7 @@ export function useStatusBarHeight() {
44
44
 
45
45
  return platformSelect({
46
46
  ios: StatusBarHeight,
47
- android: StatusBar.currentHeight,
47
+ android: StatusBar.currentHeight ?? 0,
48
48
  default: 0,
49
49
  });
50
50
  }
@@ -1,8 +1,8 @@
1
- import { useRoute } from "./useRoute";
1
+ import { useScreenContextV2 } from "../screen/useScreenContext";
2
2
  import { useMemo } from "react";
3
3
 
4
4
  export const useScreenStateStore = () => {
5
- const route = useRoute(false);
5
+ const _stateStore = useScreenContextV2()._stateStore;
6
6
 
7
- return useMemo(() => route.screenData["screenStateStore"], [route.pathname]);
7
+ return useMemo(() => _stateStore, []);
8
8
  };
@@ -1,5 +1,5 @@
1
1
  export { useRivers } from "./useRivers";
2
2
 
3
- export { useHomeRiver } from "./useHomeRiver";
3
+ export { useHomeRiver, getHomeRiver } from "./useHomeRiver";
4
4
 
5
5
  export { ZStoreProvider, useZStore } from "./ZStoreProvider";
@@ -1,8 +1,10 @@
1
- import * as R from "ramda";
2
1
  import { useRivers } from "./useRivers";
3
2
 
3
+ export const getHomeRiver = (rivers: Record<string, ZappRiver>) =>
4
+ Object.values(rivers).find((river: ZappRiver) => river.home);
5
+
4
6
  export const useHomeRiver = () => {
5
7
  const rivers = useRivers();
6
8
 
7
- return R.compose(R.find(R.propEq("home", true)), R.values)(rivers);
9
+ return getHomeRiver(rivers);
8
10
  };
@@ -0,0 +1,13 @@
1
+ export const getFocusableId = (id) => `PickerItem.${id}`;
2
+
3
+ export const getPickerSelectorId = (id) => `PickerSelector.${id}`;
4
+
5
+ export const SCREEN_PICKER_CONTAINER = "ScreenPickerContainer";
6
+
7
+ export const getScreenPickerId = (id) => `${SCREEN_PICKER_CONTAINER}.${id}`;
8
+
9
+ export const getScreenPickerSelectorContainerId = (id) =>
10
+ `${getScreenPickerId(id)}-screen-selector`;
11
+
12
+ export const getScreenPickerContentContainerId = (id) =>
13
+ `${getScreenPickerId(id)}-screen-container`;
@@ -2,6 +2,7 @@ import { BehaviorSubject } from "rxjs";
2
2
  import { SingleValueProvider } from "./StorageSingleSelectProvider";
3
3
  import { createLogger } from "../logger";
4
4
  import { bridgeLogger } from "../../zapp-react-native-bridge/logger";
5
+ import { useScreenStateStore } from "../reactHooks/navigation/useScreenStateStore";
5
6
 
6
7
  export const { log_debug, log_error } = createLogger({
7
8
  category: "ScreenSingleValueProvider",
@@ -22,7 +23,7 @@ export class ScreenSingleValueProvider implements SingleValueProvider {
22
23
  public static getProvider(
23
24
  key: string,
24
25
  screenRoute: string,
25
- screenStateStore: ScreenStateStore
26
+ screenStateStore: ReturnType<typeof useScreenStateStore>
26
27
  ): SingleValueProvider {
27
28
  if (!key) {
28
29
  throw new Error("ScreenSingleValueProvider: Key is required");
@@ -66,7 +67,7 @@ export class ScreenSingleValueProvider implements SingleValueProvider {
66
67
  private constructor(
67
68
  private key: string,
68
69
  route: string,
69
- private screenStateStore: ScreenStateStore
70
+ private screenStateStore: ReturnType<typeof useScreenStateStore>
70
71
  ) {
71
72
  if (!key) {
72
73
  throw new Error("ScreenSingleValueProvider: Key is required");
@@ -88,7 +89,9 @@ export class ScreenSingleValueProvider implements SingleValueProvider {
88
89
  log_debug("ScreenSingleValueProvider: Initializing", { key, route });
89
90
  }
90
91
 
91
- private updateStore(screenStateStore: ScreenStateStore): void {
92
+ private updateStore(
93
+ screenStateStore: ReturnType<typeof useScreenStateStore>
94
+ ): void {
92
95
  this.cleanup();
93
96
  this.screenStateStore = screenStateStore;
94
97
  this.setupScreenStateSubscription();
@@ -100,25 +103,25 @@ export class ScreenSingleValueProvider implements SingleValueProvider {
100
103
  (state) => ({
101
104
  value: state.data[this.key],
102
105
  exists: this.key in state.data,
103
- }),
104
- (current, previous) => {
105
- if (!current.exists && previous.exists) {
106
- log_debug(
107
- `ScreenSingleValueProvider: Key deleted from store: ${this.key}`
108
- );
109
-
110
- // TODO: If we need to handle deletion, we can do it here
111
- }
112
-
113
- if (current.value !== previous.value) {
114
- this.valueSubject.next(current.value || null);
115
-
116
- log_debug(`ScreenSingleValueProvider: Key updated: ${this.key}`, {
117
- previous: previous.value,
118
- current: current.value,
119
- });
120
- }
121
- }
106
+ })
107
+ // (current, previous) => {
108
+ // if (!current.exists && previous.exists) {
109
+ // log_debug(
110
+ // `ScreenSingleValueProvider: Key deleted from store: ${this.key}`
111
+ // );
112
+
113
+ // // TODO: If we need to handle deletion, we can do it here
114
+ // }
115
+
116
+ // if (current.value !== previous.value) {
117
+ // this.valueSubject.next(current.value || null);
118
+
119
+ // log_debug(`ScreenSingleValueProvider: Key updated: ${this.key}`, {
120
+ // previous: previous.value,
121
+ // current: current.value,
122
+ // });
123
+ // }
124
+ // }
122
125
  );
123
126
 
124
127
  log_debug(
@@ -2,6 +2,7 @@ import { MultiSelectProvider } from "./StorageMultiSelectProvider";
2
2
  import { BehaviorSubject } from "rxjs";
3
3
  import { createLogger } from "../logger";
4
4
  import { bridgeLogger } from "../../zapp-react-native-bridge/logger";
5
+ import { useScreenStateStore } from "../reactHooks/navigation/useScreenStateStore";
5
6
 
6
7
  export const { log_debug, log_error } = createLogger({
7
8
  category: "ScreenMultiSelectProvider",
@@ -22,7 +23,7 @@ export class ScreenMultiSelectProvider implements MultiSelectProvider {
22
23
  public static getProvider(
23
24
  key: string,
24
25
  screenRoute: string,
25
- screenStateStore: ScreenStateStore
26
+ screenStateStore: ReturnType<typeof useScreenStateStore>
26
27
  ): MultiSelectProvider {
27
28
  if (!key) {
28
29
  throw new Error("ScreenMultiSelectProvider: Key is required");
@@ -66,7 +67,7 @@ export class ScreenMultiSelectProvider implements MultiSelectProvider {
66
67
  private constructor(
67
68
  private key: string,
68
69
  route: string,
69
- private screenStateStore: ScreenStateStore
70
+ private screenStateStore: ReturnType<typeof useScreenStateStore>
70
71
  ) {
71
72
  if (!key) {
72
73
  throw new Error("ScreenMultiSelectProvider: Key is required");
@@ -88,7 +89,9 @@ export class ScreenMultiSelectProvider implements MultiSelectProvider {
88
89
  log_debug("ScreenMultiSelectProvider: Initializing", { key, route });
89
90
  }
90
91
 
91
- private updateStore(screenStateStore: ScreenStateStore): void {
92
+ private updateStore(
93
+ screenStateStore: ReturnType<typeof useScreenStateStore>
94
+ ): void {
92
95
  if (screenStateStore === this.screenStateStore) {
93
96
  return;
94
97
  }
@@ -104,26 +107,26 @@ export class ScreenMultiSelectProvider implements MultiSelectProvider {
104
107
  (state) => ({
105
108
  value: state.data[this.key],
106
109
  exists: this.key in state.data,
107
- }),
108
- (current, previous) => {
109
- if (!current.exists && previous.exists) {
110
- log_debug(
111
- `ScreenMultiSelectProvider: Key deleted from store: ${this.key}`
112
- );
113
-
114
- // TODO: If we need to handle deletion, we can do it here
115
- }
116
-
117
- if (current.value !== previous.value) {
118
- const items = this.parseStoredValue(current.value);
119
- this.itemSubject.next(items);
120
-
121
- log_debug(`ScreenMultiSelectProvider: Key updated: ${this.key}`, {
122
- previous: this.parseStoredValue(previous.value),
123
- current: items,
124
- });
125
- }
126
- }
110
+ })
111
+ // (current, previous) => {
112
+ // if (!current.exists && previous.exists) {
113
+ // log_debug(
114
+ // `ScreenMultiSelectProvider: Key deleted from store: ${this.key}`
115
+ // );
116
+
117
+ // // TODO: If we need to handle deletion, we can do it here
118
+ // }
119
+
120
+ // if (current.value !== previous.value) {
121
+ // const items = this.parseStoredValue(current.value);
122
+ // this.itemSubject.next(items);
123
+
124
+ // log_debug(`ScreenMultiSelectProvider: Key updated: ${this.key}`, {
125
+ // previous: this.parseStoredValue(previous.value),
126
+ // current: items,
127
+ // });
128
+ // }
129
+ // }
127
130
  );
128
131
 
129
132
  log_debug(
@@ -0,0 +1,30 @@
1
+ import { endsWith } from "../endsWith";
2
+
3
+ describe("endsWith", () => {
4
+ it("returns false when str is null", () => {
5
+ expect(endsWith("a", null)).toBe(false);
6
+ });
7
+
8
+ it("returns false when str is undefined", () => {
9
+ expect(endsWith("a", undefined)).toBe(false);
10
+ });
11
+
12
+ it("returns true when string ends with target", () => {
13
+ expect(endsWith("lo", "hello")).toBe(true);
14
+ expect(endsWith("", "hello")).toBe(true); // empty target always matches
15
+ });
16
+
17
+ it("returns false when string does not end with target", () => {
18
+ expect(endsWith("yo", "hello")).toBe(false);
19
+ });
20
+
21
+ it("works with single character target", () => {
22
+ expect(endsWith("o", "hello")).toBe(true);
23
+ expect(endsWith("x", "hello")).toBe(false);
24
+ });
25
+
26
+ it("is case-sensitive", () => {
27
+ expect(endsWith("Lo", "hello")).toBe(false);
28
+ expect(endsWith("lo", "hello")).toBe(true);
29
+ });
30
+ });
@@ -0,0 +1,36 @@
1
+ import { find } from "../find";
2
+
3
+ test("example 1", () => {
4
+ const predicate = <T>(_: T, index: number): boolean => index === 0;
5
+ const xs = ["1", "2", "2", "3", "4"];
6
+
7
+ expect(find(predicate, xs)).toBe("1");
8
+ });
9
+
10
+ test("example 2", () => {
11
+ const predicate = <T>(_: T, index: number): boolean => index === 0;
12
+ const xs: string[] = [];
13
+
14
+ expect(find(predicate, xs)).toBe(undefined);
15
+ });
16
+
17
+ test("example 3", () => {
18
+ const predicate = () => false;
19
+ const xs = ["1", "2", "2", "3"];
20
+
21
+ expect(find(predicate, xs)).toBe(undefined);
22
+ });
23
+
24
+ test("example 4", () => {
25
+ const predicate = <T>(_: T, index: number): boolean => index === 1;
26
+ const xs = ["1", "2", "2", "3"];
27
+
28
+ expect(find(predicate, xs)).toBe("2");
29
+ });
30
+
31
+ test("example 5", () => {
32
+ const predicate = <T>(_: T, index: number): boolean => index === 2;
33
+ const xs = ["1", "2.1", "2", "3", "2", "4"];
34
+
35
+ expect(find(predicate, xs)).toBe("2");
36
+ });
@@ -0,0 +1,19 @@
1
+ import { omit } from "../omit";
2
+
3
+ test("example 1", () => {
4
+ const path = ["a", "b", "c"];
5
+ const record = { a: 1, b: 2, c: 3 };
6
+
7
+ const output = {};
8
+
9
+ expect(omit(path, record)).toEqual(output);
10
+ });
11
+
12
+ test("example 2", () => {
13
+ const path = ["a", "b"];
14
+ const record = { a: 1, b: 2, c: 3 };
15
+
16
+ const output = { c: 3 };
17
+
18
+ expect(omit(path, record)).toEqual(output);
19
+ });