Exploring Fluent App Switcher
How-to Guide
Changed on:
22 July 2025
Changed on:
22 July 2025
1import { FC, useEffect, useState } from 'react';
2import { Link, makeStyles, MenuItem, Popover } from '@material-ui/core';
3import { MdOutlineArrowDropDown } from 'react-icons/md';
4import { useAuth } from 'mystique/hooks/useAuth';
5import { useQuery } from 'mystique/hooks/useQuery';
6import { Connection } from '../../../@types/common';
7import { getQuery } from 'mystique/hooks/getQuery';
8import { useTemplateRender } from '../../hooks/useTemplateRender';
9
10const allSettingsQuery = `
11query getAllSettings($name: String!) {
12 settings(name: [$name], context: "ACCOUNT", contextId: 0, first: 5000) {
13 edges {
14 node {
15 id
16 name
17 }
18 }
19 }
20}
21`;
22
23const settingsQuery = `
24query getSettings($name: [String!]) {
25 settings(name: $name, context: "ACCOUNT", contextId: 0, first: 5000) {
26 edges {
27 node {
28 id
29 name
30 lobValue
31 }
32 }
33 }
34}
35`;
36
37const userQuery = `
38query getUser($username: String!) {
39 users(username: [$username]) {
40 edges {
41 node {
42 roles {
43 contexts {
44 contextType
45 }
46 }
47 }
48 }
49 }
50}
51`;
52
53interface SettingsResult {
54 settings: Connection<SettingsNode>;
55}
56
57interface SettingsNode {
58 id: number;
59 name: string;
60 lobValue: any;
61 path: string;
62 description: string;
63}
64
65interface AppSwitcherProps {
66 exclude: string[];
67 include: [
68 {
69 name: string;
70 url: string;
71 newTab?: boolean;
72 },
73 ];
74}
75
76const AppSwitcher: FC<AppSwitcherProps> = ({ include, exclude }) => {
77 const manifestPrefix = 'fc.mystique.manifest.';
78
79 const [anchorEl, setAnchorEl] = useState(null);
80 const auth = useAuth();
81 const templateRender = useTemplateRender();
82 const classes = useStyles();
83 const [settings] = useQuery<SettingsResult>(allSettingsQuery, {
84 name: manifestPrefix + '%',
85 });
86 const [user] = useQuery<any>(userQuery, {
87 username: auth.user.username,
88 });
89 const [loading, setLoading] = useState(false);
90 const [applications, setApplications] = useState<SettingsNode[]>();
91
92 const open = Boolean(anchorEl);
93
94 const handleClick = (event: any) => {
95 setAnchorEl(event.currentTarget);
96 };
97
98 const handleClose = () => {
99 setAnchorEl(null);
100 };
101
102 useEffect(() => {
103 if (!settings.fetching && !user.fetching) {
104 const apps: string[] = [];
105 settings?.data?.settings.edges.forEach((setting) => {
106 const appName = setting.node.name.split(manifestPrefix)[1];
107 if (appName.indexOf('.') < 0) {
108 apps.push(manifestPrefix + appName);
109 }
110 });
111 setLoading(true);
112 getQuery<SettingsResult>(settingsQuery, {
113 name: apps,
114 }).then((resp) => {
115 setLoading(false);
116 if (!resp.error) {
117 const applications: SettingsNode[] = [];
118 const location = window.location.pathname;
119 const contexts: string[] = [];
120 user.data.users.edges[0].node.roles.forEach((role: any) => {
121 role.contexts.forEach((context: any) => {
122 const c =
123 context.contextType === 'AGENT'
124 ? 'location'
125 : context.contextType.toLowerCase();
126 if (!contexts.includes(c)) {
127 contexts.push(c);
128 }
129 });
130 });
131 resp?.data?.settings.edges.forEach((app) => {
132 const appName = app.node.name
133 .split(manifestPrefix)[1]
134 .toLowerCase();
135 if (
136 location.toLowerCase() !== '/' + appName &&
137 (!exclude || !exclude.includes(appName))
138 ) {
139 app.node.path = appName;
140 const manifestLevel = app.node.lobValue.context
141 ? app.node.lobValue.context.level.toLowerCase()
142 : 'retailer';
143 if (contexts.includes(manifestLevel)) {
144 applications.push(app.node);
145 }
146 }
147 });
148 setApplications(applications);
149 }
150 });
151 }
152 }, [settings, user]);
153
154 return (
155 <>
156 {!settings.fetching &&
157 !user.fetching &&
158 !loading &&
159 ((applications && applications.length > 0) ||
160 (include && include.length > 0)) && (
161 <>
162 <MdOutlineArrowDropDown
163 className={classes.menuOpen}
164 size={24}
165 onClick={handleClick}
166 />
167 <Popover
168 open={open}
169 anchorEl={anchorEl}
170 onClose={handleClose}
171 anchorOrigin={{
172 vertical: 'bottom',
173 horizontal: 'center',
174 }}
175 transformOrigin={{
176 vertical: 'top',
177 horizontal: 'center',
178 }}
179 >
180 {applications?.map((value: SettingsNode, idx: number) => {
181 return (
182 <Link className={classes.link} key={idx} href={value.path}>
183 <MenuItem className={classes.menuItem} key={idx}>
184 {value.lobValue.title
185 ? value.lobValue.title
186 : value.lobValue.name}
187 </MenuItem>
188 </Link>
189 );
190 })}
191 {include?.map((value: any, idx: number) => {
192 return (
193 <Link
194 className={classes.link}
195 key={idx}
196 href={templateRender(value.url)}
197 target={
198 value.newTab === undefined || value.newTab
199 ? '_blank'
200 : undefined
201 }
202 >
203 <MenuItem className={classes.menuItem} key={idx}>
204 {templateRender(value.name)}
205 </MenuItem>
206 </Link>
207 );
208 })}
209 </Popover>
210 </>
211 )}
212 </>
213 );
214};
215
216const useStyles = makeStyles((theme) => ({
217 menuOpen: {
218 paddingTop: '6px',
219 cursor: 'pointer',
220 },
221 link: {
222 backgroundColor: theme.palette.secondary.main,
223 color: 'white',
224 },
225 menuItem: {
226 '&:hover': {
227 backgroundColor: theme.palette.secondary.main,
228 },
229 backgroundColor: theme.palette.secondary.main,
230 },
231}));
232
233export default AppSwitcher;1 {
2 position: 'left',
3 component: 'fc.se.app.switcher',
4 props: {
5 exclude: ['labstore', 'labconsole'],
6 include: [
7 {
8 name: "{{i18n 'fc.se.app.switcher.console'}}",
9 url: 'https://{{env.account}}.sandbox.console.fluentretail.com/',
10 },
11 ],
12 },
13 }