@availity/mui-spaces 0.1.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.
@@ -0,0 +1,116 @@
1
+ import { avWebQLApi } from '@availity/api-axios';
2
+ import {
3
+ Space,
4
+ NameValuePair,
5
+ NormalizedSpace,
6
+ NormalizedPairField,
7
+ PairFields,
8
+ FetchSpacesProps,
9
+ FetchAllSpacesProps,
10
+ SpacesContextType,
11
+ SpacesReducerAction,
12
+ } from './spaces-types';
13
+
14
+ export const actions = {
15
+ SPACES: (
16
+ currentState: SpacesContextType,
17
+ { spaces, spacesByConfig, spacesByPayer }: SpacesReducerAction
18
+ ): SpacesContextType => ({
19
+ previousSpacesMap: spaces,
20
+ previousSpacesByConfigMap: spacesByConfig,
21
+ previousSpacesByPayerMap: spacesByPayer,
22
+ error: undefined,
23
+ loading: false,
24
+ }),
25
+ ERROR: (state: SpacesContextType, { error }: SpacesReducerAction): SpacesContextType => ({
26
+ ...state,
27
+ loading: false,
28
+ error,
29
+ }),
30
+ LOADING: (state: SpacesContextType, { loading }: SpacesReducerAction): SpacesContextType => ({
31
+ ...state,
32
+ loading: loading !== undefined ? loading : !state.loading,
33
+ }),
34
+ };
35
+
36
+ export const spacesReducer = (state: SpacesContextType, action: SpacesReducerAction): SpacesContextType => {
37
+ const { type } = action;
38
+ return actions[type](state, action);
39
+ };
40
+
41
+ export const normalizeSpaces = (spaces: (Space | Space[] | undefined)[]): NormalizedSpace[] => {
42
+ // if spaces coming in is array of an array of spaces objects,
43
+ // then we matched by payerId and should unravel that first level of array
44
+ let spacesToReduce = spaces;
45
+ if (Array.isArray(spaces[0])) {
46
+ spacesToReduce = spaces[0];
47
+ }
48
+ // Normalize space pairs ( [{ name: 'foo'', value: 'bar' }] => { foo: 'bar' } )
49
+ const pairFields: PairFields = ['images', 'metadata', 'colors', 'icons', 'mapping'];
50
+ return spacesToReduce.reduce((accum: NormalizedSpace[], spc: Space): NormalizedSpace[] => {
51
+ if (!spc) return accum;
52
+
53
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
54
+ const { images, metadata, colors, icons, mapping, ...rest } = spc;
55
+ const normalizedSpace: NormalizedSpace = { ...rest };
56
+
57
+ for (const field of pairFields) {
58
+ if (spc[field] && Array.isArray(spc[field])) {
59
+ normalizedSpace[field] = spc[field]?.reduce((_accum: NormalizedPairField, { name, value }: NameValuePair) => {
60
+ _accum[name] = value;
61
+ return _accum;
62
+ }, {});
63
+ }
64
+ }
65
+
66
+ accum.push(normalizedSpace);
67
+ return accum;
68
+ }, []);
69
+ };
70
+
71
+ export const fetchSpaces = async ({ query, clientId, variables }: FetchSpacesProps) => {
72
+ // eslint-disable-next-line no-useless-catch
73
+ const {
74
+ data: {
75
+ data: { configurationPagination },
76
+ },
77
+ } = await avWebQLApi.create(
78
+ {
79
+ query,
80
+ variables: { ...variables },
81
+ },
82
+ { headers: { 'X-Client-ID': clientId } }
83
+ );
84
+
85
+ const {
86
+ pageInfo: { currentPage, hasNextPage },
87
+ items,
88
+ } = configurationPagination;
89
+
90
+ return {
91
+ items,
92
+ currentPage,
93
+ hasNextPage,
94
+ };
95
+ };
96
+
97
+ export const fetchAllSpaces = async ({
98
+ query,
99
+ clientId,
100
+ variables,
101
+ _spaces = [],
102
+ }: FetchAllSpacesProps): Promise<Space[]> => {
103
+ const { items, currentPage, hasNextPage } = await fetchSpaces({ query, clientId, variables });
104
+
105
+ _spaces.push(...items);
106
+
107
+ if (hasNextPage) {
108
+ const vars = {
109
+ ...variables,
110
+ page: currentPage + 1,
111
+ };
112
+ return fetchAllSpaces({ query, clientId, variables: vars, _spaces });
113
+ }
114
+
115
+ return _spaces;
116
+ };
@@ -0,0 +1,136 @@
1
+ export type Link = {
2
+ /** Contains a URL or URL Fragment that the hyperlink points to. */
3
+ url: string;
4
+ /** Specifies where to open the linked URL. */
5
+ target: string;
6
+ };
7
+
8
+ export type NameValuePair = {
9
+ /** The key of the of the data. */
10
+ name: string;
11
+ /** The value of the data. */
12
+ value: string;
13
+ };
14
+
15
+ export type PairFields = ('images' | 'metadata' | 'colors' | 'icons' | 'mapping')[];
16
+
17
+ export type Space = {
18
+ /** The name of the content or configuration. */
19
+ name: string;
20
+ /** The configuration type. */
21
+ type: string;
22
+ /** Globally unique ID associated with the configuration object. */
23
+ id: string;
24
+ /** The ID used to identify the configuration. */
25
+ configurationId: string;
26
+ /** A list of payerIds for the configuration or content. */
27
+ payerIDs?: string[];
28
+ /** The images associated with the configuration. */
29
+ images?: NameValuePair[];
30
+ /** Key-value data for a configuration. */
31
+ metadata?: NameValuePair[];
32
+ /** The feature box colors associated with the Payer Space. */
33
+ colors?: NameValuePair[];
34
+ /** Contains URL fragments that point to icons. */
35
+ icons?: NameValuePair[];
36
+ /** The key-value mapping pairs. */
37
+ mapping?: NameValuePair[];
38
+ };
39
+
40
+ export type NormalizedPairField = { [key: string]: string };
41
+
42
+ export interface NormalizedSpace extends Omit<Space, 'images' | 'metadata' | 'colors' | 'icons' | 'mapping'> {
43
+ /** The images associated with the configuration. */
44
+ images?: NormalizedPairField;
45
+ /** The feature box colors associated with the Payer Space. */
46
+ colors?: NormalizedPairField;
47
+ /** Contains URL fragments that point to icons. */
48
+ icons?: NormalizedPairField;
49
+ /** Key-value data for a configuration. */
50
+ metadata?: NormalizedPairField;
51
+ /** The key-value mapping pairs. */
52
+ mapping?: NormalizedPairField;
53
+ /** URL metadata for the configuration. */
54
+ link?: {
55
+ /** Contains a URL or URL Fragment that the hyperlink points to. */
56
+ url: string;
57
+ /** Specifies where to open the linked URL. */
58
+ target: string;
59
+ };
60
+ }
61
+
62
+ export type FetchSpacesProps = {
63
+ /** The query sent to the avWebQL endpoint. */
64
+ query: string;
65
+ /** The Client ID obtained from APIConnect. Must be subscribed to the thanos API. */
66
+ clientId: string;
67
+ /** The variables sent to the avWebQL endpoint. */
68
+ variables?: object;
69
+ /** The page sent to the avWebQL endpoint. */
70
+ page?: string;
71
+ };
72
+
73
+ export type FetchAllSpacesProps = {
74
+ /** The query sent to the avWebQL endpoint. */
75
+ query: string;
76
+ /** The Client ID obtained from APIConnect. Must be subscribed to the thanos API. */
77
+ clientId: string;
78
+ /** The variables sent to the avWebQL endpoint. */
79
+ variables?: object;
80
+ /** Array of spaces to be passed into the Spaces provider. */
81
+ _spaces?: Space[];
82
+ };
83
+
84
+ export type SpacesContextType = {
85
+ /** Array of spaces to be passed into the Spaces provider. */
86
+ spaces?: Map<string, Space>;
87
+ /** Array of spaces from previous page load. */
88
+ previousSpacesMap?: Map<string, Space>;
89
+ /** Array of spaces organized by configurationId. */
90
+ spacesByConfig?: Map<string, Space>;
91
+ /** Array of spaces organized by configurationId from previous page load. */
92
+ previousSpacesByConfigMap?: Map<string, Space>;
93
+ /** Array of spaces organized by payerId. */
94
+ spacesByPayer?: Map<string, Space[]>;
95
+ /** Array of spaces organized by payerId from previous page load. */
96
+ previousSpacesByPayerMap?: Map<string, Space[]>;
97
+ /** Whether or not spaces are loading. */
98
+ loading: boolean;
99
+ /** Errors associated with fetching spaces. */
100
+ error?: string;
101
+ /** Items that live within the spaces component and can access SpacesContext. */
102
+ children?: React.ReactNode;
103
+ };
104
+
105
+ export interface SpacesReducerAction extends SpacesContextType {
106
+ /** Action to take on SpacesContext state. */
107
+ type: 'SPACES' | 'ERROR' | 'LOADING';
108
+ }
109
+
110
+ export type ChildrenProps = {
111
+ /** Array of spaces to be passed into the Spaces provider. */
112
+ spaces: NormalizedSpace[];
113
+ };
114
+
115
+ export type SpacesProps = {
116
+ /** Override the default thanos query. */
117
+ query?: string;
118
+ /** Override the default variables used in the thanos query. Default: { types: [PAYERSPACE] }.
119
+ * If the spaces provider should contain configurations of a type other than PAYERSPACE, you must override this prop */
120
+ variables?: object;
121
+ /** The Client ID obtained from APIConnect. Must be subscribed to the thanos API. */
122
+ clientId: string;
123
+ /** Children can be a react child or render prop. */
124
+ children?: React.ReactNode;
125
+ /** Array of payerIds the Spaces provider should fetch the spaces for.
126
+ * Any payerIds already included in spaces will not be fetched again.
127
+ * Note: If a payerId is associated with more than one payer space, the order in which they are returned should not be relied upon.
128
+ * If a specific payer space is required, you'll need to filter the list that is returned. */
129
+ payerIds?: string[];
130
+ /** Array of spaceIds the Spaces provider should fetch the spaces for.
131
+ * Any spaceIds already included in spaces will not be fetched again. */
132
+ spaceIds?: string[];
133
+ /** Array of spaces to be passed into the Spaces provider.
134
+ * Useful for if you already have the spaces in your app and don't want the spaces provider to have to fetch them again. */
135
+ spaces?: Space[];
136
+ };
@@ -0,0 +1,93 @@
1
+ import { cleanup } from '@testing-library/react';
2
+ import { updateTopApps } from './topApps';
3
+ import { Space } from './spaces-types';
4
+
5
+ const localStorageMock = (() => {
6
+ let store: Record<string, string> = {};
7
+ return {
8
+ getItem(key: string) {
9
+ return store[key];
10
+ },
11
+ setItem(key: string, value: Space) {
12
+ store[key] = value.toString();
13
+ },
14
+ clear() {
15
+ store = {};
16
+ },
17
+ removeItem(key: string) {
18
+ delete store[key];
19
+ },
20
+ };
21
+ })();
22
+
23
+ describe('updateTopApps', () => {
24
+ beforeEach(() => {
25
+ jest.useFakeTimers().setSystemTime(new Date('2022-01-01').getTime());
26
+ Object.defineProperty(window, 'localStorage', { value: localStorageMock });
27
+ });
28
+ afterEach(() => {
29
+ jest.clearAllMocks();
30
+ window.localStorage.clear();
31
+ cleanup();
32
+ });
33
+
34
+ it('should updateTopApps for applications', async () => {
35
+ await updateTopApps(
36
+ { id: 'space1', configurationId: 'space1', name: 'space1', type: 'APPLICATION' },
37
+ 'aka123456789'
38
+ );
39
+
40
+ expect(window.localStorage.getItem('myTopApps-aka123456789')).toEqual(
41
+ '{"space1":{"count":1,"lastUse":"2022-01-01T00:00:00+00:00"}}'
42
+ );
43
+ });
44
+
45
+ it('should updateTopApps for resources', async () => {
46
+ await updateTopApps({ id: 'space2', configurationId: 'space2', name: 'space2', type: 'RESOURCE' }, 'aka123456789');
47
+
48
+ expect(window.localStorage.getItem('myTopApps-aka123456789')).toEqual(
49
+ '{"space2":{"count":1,"lastUse":"2022-01-01T00:00:00+00:00"}}'
50
+ );
51
+ });
52
+
53
+ it('should updateTopApps for navigations', async () => {
54
+ await updateTopApps(
55
+ { id: 'space3', configurationId: 'space3', name: 'space3', type: 'NAVIGATION' },
56
+ 'aka123456789'
57
+ );
58
+
59
+ expect(window.localStorage.getItem('myTopApps-aka123456789')).toEqual(
60
+ '{"space3":{"count":1,"lastUse":"2022-01-01T00:00:00+00:00"}}'
61
+ );
62
+ });
63
+
64
+ it('should updateTopApps for multiple clicks', async () => {
65
+ await updateTopApps(
66
+ { id: 'space1', configurationId: 'space1', name: 'space1', type: 'APPLICATION' },
67
+ 'aka123456789'
68
+ );
69
+ await updateTopApps(
70
+ { id: 'space1', configurationId: 'space1', name: 'space1', type: 'APPLICATION' },
71
+ 'aka123456789'
72
+ );
73
+
74
+ expect(window.localStorage.getItem('myTopApps-aka123456789')).toEqual(
75
+ '{"space1":{"count":2,"lastUse":"2022-01-01T00:00:00+00:00"}}'
76
+ );
77
+ });
78
+
79
+ it('should not updateTopApps if type is not allowed', async () => {
80
+ await updateTopApps({ id: 'space2', configurationId: 'space2', name: 'space2', type: 'SAML' }, 'aka123456789');
81
+
82
+ expect(window.localStorage.getItem('myTopApps-aka123456789')).toEqual(undefined);
83
+ });
84
+
85
+ it('should not updateTopApps if id is blacklisted', async () => {
86
+ await updateTopApps(
87
+ { id: 'reporting', configurationId: 'reporting', name: 'reporting', type: 'APPLICATION' },
88
+ 'aka123456789'
89
+ );
90
+
91
+ expect(window.localStorage.getItem('myTopApps-aka123456789')).toEqual(undefined);
92
+ });
93
+ });
@@ -0,0 +1,65 @@
1
+ import avMessage from '@availity/message-core';
2
+ import dayjs from 'dayjs';
3
+ import { Space } from './spaces-types';
4
+
5
+ const TOP_APPS = {
6
+ ALLOWED_TYPES: ['APPLICATION', 'RESOURCE', 'NAVIGATION'],
7
+ DISALLOWED_TYPES: ['reporting', 'how_to_guide_dental_providers', 'my_account_profile', 'my_administrators'],
8
+ KEYS: {
9
+ VALUES: 'myTopApps',
10
+ LAST_UPDATED: 'top-apps-updated',
11
+ },
12
+ UPDATE_EVENT: 'av:topApps:updated',
13
+ };
14
+
15
+ const getItemLocalStorage = (key: string) => {
16
+ try {
17
+ const value = window.localStorage.getItem(key);
18
+ if (value === null) {
19
+ return null;
20
+ }
21
+
22
+ return JSON.parse(value);
23
+ } catch {
24
+ return null;
25
+ }
26
+ };
27
+
28
+ const canTrackSpace = (spaceId: string, type: string) =>
29
+ TOP_APPS.ALLOWED_TYPES.some((t) => t === type) && !TOP_APPS.DISALLOWED_TYPES.some((id) => id === spaceId);
30
+
31
+ const getLocalStorageTopApps = (akaname: string) => {
32
+ const topAppsValues = getItemLocalStorage(`${TOP_APPS.KEYS.VALUES}-${akaname}`);
33
+
34
+ return topAppsValues;
35
+ };
36
+
37
+ export const updateTopApps = async (space: Space, akaname: string) => {
38
+ if (!space.configurationId || !space.type) return;
39
+
40
+ // If we can track the space
41
+ if (canTrackSpace(space.configurationId, space.type)) {
42
+ const today = dayjs();
43
+
44
+ // Grab the current top apps from localstorage
45
+ const topApps = (await getLocalStorageTopApps(akaname)) || {};
46
+
47
+ // Update the last updated date. For use in top nav to actually sync with settings api
48
+ window.localStorage.setItem(`${TOP_APPS.KEYS.LAST_UPDATED}-${akaname}`, today.format());
49
+
50
+ const currentCount =
51
+ topApps[space.configurationId] && typeof topApps[space.configurationId].count === 'number'
52
+ ? topApps[space.configurationId].count
53
+ : 0;
54
+
55
+ topApps[space.configurationId] = {
56
+ ...topApps?.[space.configurationId],
57
+ count: currentCount + 1,
58
+ lastUse: today.format(),
59
+ };
60
+
61
+ window.localStorage.setItem(`${TOP_APPS.KEYS.VALUES}-${akaname}`, JSON.stringify(topApps));
62
+
63
+ avMessage.send(TOP_APPS.UPDATE_EVENT);
64
+ }
65
+ };
package/tsconfig.json ADDED
@@ -0,0 +1,5 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "include": ["."],
4
+ "exclude": ["dist", "build", "node_modules"]
5
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "outDir": "../../dist/out-tsc",
5
+ "module": "commonjs",
6
+ "types": ["jest", "node", "@testing-library/jest-dom"],
7
+ "allowJs": true
8
+ },
9
+ "include": ["**/*.test.js", "**/*.test.ts", "**/*.test.tsx", "**/*.d.ts"]
10
+ }