@jsb188/mday-web 1.1.8

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/package.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "@jsb188/mday-web",
3
+ "version": "1.1.8",
4
+ "description": "Web components and utilities for Marketday app.",
5
+ "license": "ISC",
6
+ "author": "jsb188",
7
+ "type": "module",
8
+ "exports": {
9
+ "./utils/route": "./src/utils/route-helpers.ts"
10
+ },
11
+ "dependencies": {},
12
+ "peerDependencies": {
13
+ "@jsb188/app": "1.1.8",
14
+ "@jsb188/mday": "1.1.8",
15
+ "@jsb188/react-web": "1.1.8"
16
+ },
17
+ "scripts": {
18
+ "test": "echo \"Error: no test specified\" && exit 1"
19
+ }
20
+ }
@@ -0,0 +1,602 @@
1
+ import i18n from '@jsb188/app/i18n';
2
+ import { COMMON_ICON_NAMES } from '@jsb188/react-web/svgs/Icon';
3
+ import type { OrganizationFeatureEnum, OrganizationOperationEnum } from '../types/organization.d';
4
+
5
+ // Use this for report periods, etc
6
+ const CURRENT_YEAR = String(new Date().getFullYear());
7
+
8
+ /**
9
+ * Constants; Re-usable rules
10
+ */
11
+
12
+ const OP_FARMING = ['ARABLE', 'LIVESTOCK'];
13
+
14
+ const F = {
15
+ normal_logging: ['NORMAL_LOGGING'],
16
+ food_safety: ['FOOD_SAFETY', 'GLOBAL_GAP'],
17
+ };
18
+
19
+ /**
20
+ * Rules
21
+ */
22
+
23
+ type ValidRoutePath =
24
+ '/app'
25
+ | '/app/c/'
26
+ | '/app/ai-workflows'
27
+ | '/app/logs'
28
+ | '/app/seeding'
29
+ | '/app/transplanting'
30
+ | '/app/field-work'
31
+ | '/app/harvested'
32
+ | '/app/post-harvest'
33
+ | '/app/orders'
34
+ | '/app/globalgap/'
35
+ | '/app/cleaning'
36
+ | '/app/purchases'
37
+ | '/app/organic'
38
+ | '/app/hygiene'
39
+ | '/app/sanitation'
40
+ | '/app/materials'
41
+ | '/app/biosecurity'
42
+ | '/app/employees'
43
+ | '/app/vendors'
44
+ | '/app/markets'
45
+ | '/app/receipts'
46
+ | '/app/livestock'
47
+ | '/app/growers'
48
+ | '/app/foreign-growers';
49
+
50
+ interface RouteDictObj {
51
+ to: ValidRoutePath;
52
+ text: string;
53
+ iconName?: string;
54
+ allowedOperations?: OrganizationOperationEnum[];
55
+ notAllowedOperations?: OrganizationOperationEnum[];
56
+ requiredFeature?: OrganizationFeatureEnum[];
57
+
58
+ // These values prevent rendering flickers when calculating TOC/breadcrumbs between page renders
59
+ hasPhysicalToolbar?: 'ALWAYS' | 'NEVER' | ((parts: string[]) => boolean);
60
+ hasAsideNav?: 'ALWAYS' | 'NEVER' | ((parts: string[]) => boolean);
61
+ }
62
+
63
+ const ROUTES_DICT: Record<ValidRoutePath, RouteDictObj> = {
64
+
65
+ // Main /app/ routes
66
+
67
+ '/app': {
68
+ to: '/app',
69
+ text: 'app.home',
70
+ },
71
+
72
+ '/app/c/': {
73
+ to: '/app/c/',
74
+ text: 'app.route_ai_chat',
75
+ iconName: COMMON_ICON_NAMES.chat,
76
+
77
+ hasPhysicalToolbar: 'ALWAYS',
78
+ hasAsideNav: 'NEVER',
79
+ },
80
+
81
+ // Advanced
82
+
83
+ '/app/ai-workflows': {
84
+ to: '/app/ai-workflows',
85
+ text: 'form.ai_workflows',
86
+ iconName: COMMON_ICON_NAMES.ai_workflow,
87
+ },
88
+
89
+ '/app/logs': {
90
+ to: '/app/logs',
91
+ text: 'log.all_logs',
92
+ iconName: COMMON_ICON_NAMES.logs,
93
+ notAllowedOperations: ['GROWER_NETWORK'], // Temporary for now
94
+ },
95
+
96
+ // Arable
97
+
98
+ '/app/seeding': {
99
+ to: '/app/seeding',
100
+ text: 'log.seeding',
101
+ iconName: COMMON_ICON_NAMES.seeding,
102
+
103
+ allowedOperations: ['ARABLE'],
104
+ requiredFeature: F.normal_logging,
105
+ },
106
+ '/app/transplanting': {
107
+ to: '/app/transplanting',
108
+ text: 'log.transplanting',
109
+ iconName: COMMON_ICON_NAMES.transplanting,
110
+
111
+ allowedOperations: ['ARABLE'],
112
+ requiredFeature: F.normal_logging,
113
+ },
114
+ '/app/field-work': {
115
+ to: '/app/field-work',
116
+ text: 'log.field_work',
117
+ iconName: COMMON_ICON_NAMES.field_work,
118
+
119
+ allowedOperations: ['ARABLE'],
120
+ requiredFeature: F.normal_logging,
121
+ },
122
+ '/app/harvested': {
123
+ to: '/app/harvested',
124
+ text: 'log.harvested',
125
+ iconName: COMMON_ICON_NAMES.harvest,
126
+
127
+ allowedOperations: ['ARABLE'],
128
+ requiredFeature: F.normal_logging,
129
+ },
130
+ '/app/post-harvest': {
131
+ to: '/app/post-harvest',
132
+ text: 'log.post_harvest',
133
+ iconName: COMMON_ICON_NAMES.post_harvest,
134
+
135
+ allowedOperations: ['ARABLE'],
136
+ requiredFeature: F.normal_logging,
137
+ },
138
+ '/app/purchases': {
139
+ to: '/app/purchases',
140
+ text: 'log.purchases',
141
+ iconName: COMMON_ICON_NAMES.invoice,
142
+
143
+ allowedOperations: OP_FARMING,
144
+ requiredFeature: F.normal_logging,
145
+ },
146
+ '/app/orders': {
147
+ to: '/app/orders',
148
+ text: 'form.sales_orders',
149
+ iconName: COMMON_ICON_NAMES.receipt,
150
+
151
+ allowedOperations: OP_FARMING,
152
+ requiredFeature: F.normal_logging,
153
+ },
154
+
155
+ // Arable; Reports
156
+
157
+ '/app/globalgap/': {
158
+ to: ('/app/globalgap/' + CURRENT_YEAR) as ValidRoutePath,
159
+ text: 'product.report.GLOBAL_GAP',
160
+ iconName: COMMON_ICON_NAMES.generic_report,
161
+
162
+ allowedOperations: OP_FARMING,
163
+ requiredFeature: ['GLOBAL_GAP'],
164
+
165
+ hasPhysicalToolbar: (parts: string[]) => parts.length > 4,
166
+ hasAsideNav: (parts: string[]) => parts.length > 4,
167
+ },
168
+ '/app/cleaning': {
169
+ to: '/app/cleaning',
170
+ text: 'product.report.CLEANING',
171
+ iconName: COMMON_ICON_NAMES.SANITATION,
172
+
173
+ allowedOperations: OP_FARMING,
174
+ requiredFeature: F.food_safety,
175
+ },
176
+ '/app/organic': {
177
+ to: '/app/organic',
178
+ text: 'product.report.ORGANIC_CERTIFICATION',
179
+ iconName: COMMON_ICON_NAMES.generic_report,
180
+
181
+ allowedOperations: OP_FARMING,
182
+ requiredFeature: ['ORGANIC_CERTIFICATION'],
183
+
184
+ // This will need to change later if we're introducing "period" table of contents page selection
185
+ hasPhysicalToolbar: 'ALWAYS',
186
+ hasAsideNav: 'ALWAYS',
187
+ },
188
+
189
+ // Arable; Food Safety
190
+
191
+ '/app/hygiene': {
192
+ to: '/app/hygiene',
193
+ text: 'log.hygiene',
194
+ iconName: COMMON_ICON_NAMES.HYGIENE,
195
+
196
+ allowedOperations: OP_FARMING,
197
+ requiredFeature: F.food_safety,
198
+ },
199
+ '/app/sanitation': {
200
+ to: '/app/sanitation',
201
+ text: 'log.sanitation',
202
+ iconName: COMMON_ICON_NAMES.SANITATION,
203
+
204
+ allowedOperations: OP_FARMING,
205
+ requiredFeature: F.food_safety,
206
+ },
207
+ '/app/materials': {
208
+ to: '/app/materials',
209
+ text: 'log.materials',
210
+ iconName: COMMON_ICON_NAMES.MATERIALS,
211
+
212
+ allowedOperations: OP_FARMING,
213
+ requiredFeature: F.food_safety,
214
+ },
215
+ '/app/biosecurity': {
216
+ to: '/app/biosecurity',
217
+ text: 'log.biosecurity',
218
+ iconName: COMMON_ICON_NAMES.BIOSECURITY,
219
+
220
+ allowedOperations: OP_FARMING,
221
+ requiredFeature: F.food_safety,
222
+ },
223
+ '/app/employees': {
224
+ to: '/app/employees',
225
+ text: 'log.employees',
226
+ iconName: COMMON_ICON_NAMES.EMPLOYEES,
227
+
228
+ allowedOperations: OP_FARMING,
229
+ requiredFeature: F.food_safety,
230
+ },
231
+
232
+ // Farmers Market
233
+ '/app/vendors': {
234
+ to: '/app/vendors',
235
+ text: 'form.vendors',
236
+ iconName: COMMON_ICON_NAMES.shop_vendor,
237
+
238
+ allowedOperations: ['FARMERS_MARKET'],
239
+ requiredFeature: F.normal_logging
240
+ },
241
+ '/app/markets': {
242
+ to: '/app/markets',
243
+ text: 'form.markets',
244
+ iconName: COMMON_ICON_NAMES.shop_market,
245
+
246
+ allowedOperations: ['FARMERS_MARKET'],
247
+ requiredFeature: ['CAL_EVENTS'],
248
+ },
249
+ '/app/receipts': {
250
+ to: ('/app/receipts?s=1') as ValidRoutePath,
251
+ text: 'form.market_receipts',
252
+ iconName: COMMON_ICON_NAMES.market_receipt,
253
+
254
+ allowedOperations: ['FARMERS_MARKET'],
255
+ requiredFeature: F.normal_logging
256
+ },
257
+
258
+ // Livestock
259
+ '/app/livestock': {
260
+ to: '/app/livestock',
261
+ text: 'form.livestock',
262
+ iconName: COMMON_ICON_NAMES.livestock,
263
+
264
+ allowedOperations: ['LIVESTOCK'],
265
+ requiredFeature: F.normal_logging
266
+ },
267
+
268
+ // Grower Network
269
+ '/app/growers': {
270
+ to: '/app/growers',
271
+ text: 'form.domestic_growers',
272
+ iconName: COMMON_ICON_NAMES.growers,
273
+
274
+ allowedOperations: ['GROWER_NETWORK'],
275
+ },
276
+ '/app/foreign-growers': {
277
+ to: '/app/foreign-growers',
278
+ text: 'form.foreign_growers',
279
+ iconName: COMMON_ICON_NAMES.foreign_growers,
280
+
281
+ allowedOperations: ['GROWER_NETWORK'],
282
+ },
283
+ };
284
+
285
+ /**
286
+ * Make pathname to a route in Marketday app.
287
+ * @param routeName - The name of the route.
288
+ * @param pathSegment - Optional additional path segment to append.
289
+ * @returns The full pathname for the route.
290
+ */
291
+
292
+ export function makePathname(routePath: ValidRoutePath, pathSegment?: string | null): ValidRoutePath {
293
+ const pathExists = !!ROUTES_DICT[routePath];
294
+ if (pathExists && routePath.endsWith('/')) {
295
+ return (pathSegment ? `${routePath}${pathSegment}` : routePath.substring(0, routePath.length - 1)) as ValidRoutePath;
296
+ }
297
+ return pathExists ? routePath : '/app';
298
+ }
299
+
300
+ /**
301
+ * Check if route is valid.
302
+ * @param routeName - The name of the route.
303
+ * @param pathSegment - Optional additional path segment to append.
304
+ * @returns The full pathname for the route.
305
+ */
306
+
307
+ export function isRouteValid(routePath: ValidRoutePath, pathSegment?: string | null): boolean {
308
+ const routeDict = !!ROUTES_DICT[routePath];
309
+ return (
310
+ !!routeDict &&
311
+ (!routePath.endsWith('/') || !!pathSegment)
312
+ );
313
+ }
314
+
315
+ /**
316
+ * Get route config from pathname.
317
+ * NOTE: This is the best way to prevent page flickers due to breadcrumbs/TOC calculations/resets/etc.
318
+ * @param pathname - The pathname to check.
319
+ * @returns The route name if found.
320
+ */
321
+
322
+ const ROUTES_DICT_ORDERED = Object.keys(ROUTES_DICT).sort((a, b) => b.length - a.length);
323
+
324
+ interface RouteConfigObj extends Omit<RouteDictObj, 'hasPhysicalToolbar' | 'hasAsideNav'> {
325
+ routeName: ValidRoutePath;
326
+ scrollResetKey: string;
327
+ allowed: boolean;
328
+ hasPhysicalToolbar: boolean;
329
+ hasAsideNav: boolean;
330
+ }
331
+
332
+ export function getRouteConfigs(
333
+ pathname: ValidRoutePath | string,
334
+ operation?: OrganizationOperationEnum | null,
335
+ orgFeatures?: OrganizationFeatureEnum[] | null,
336
+ ): RouteConfigObj {
337
+
338
+ for (const routeName of ROUTES_DICT_ORDERED) {
339
+ if (pathname.startsWith(routeName)) {
340
+
341
+ // Scroll position fix + breadcrumb / TOC cleanup on unmount
342
+ // This will retain scroll position for deeper links,
343
+ // ie. "/app/globalgap/2023/.." will be retained
344
+
345
+ const routeDict = ROUTES_DICT[routeName as ValidRoutePath];
346
+ const pathParts = pathname.split('/');
347
+
348
+ let scrollResetKey: string;
349
+ if (pathParts.length >= 3) {
350
+ // scroll reset key here is full path minus last part
351
+ scrollResetKey = pathParts.slice(0, pathParts.length - 1).join('/');
352
+ } else {
353
+ scrollResetKey = pathParts.slice(0, 3).join('/');
354
+ }
355
+
356
+ const hasAsideNav = !!routeDict.hasAsideNav && routeDict.hasAsideNav !== 'NEVER' && (
357
+ routeDict.hasAsideNav === 'ALWAYS' ||
358
+ (typeof routeDict.hasAsideNav === 'function' && routeDict.hasAsideNav(pathParts))
359
+ );
360
+
361
+ const hasPhysicalToolbar = !!routeDict.hasPhysicalToolbar && routeDict.hasPhysicalToolbar !== 'NEVER' && (
362
+ routeDict.hasPhysicalToolbar === 'ALWAYS' ||
363
+ (typeof routeDict.hasPhysicalToolbar === 'function' && routeDict.hasPhysicalToolbar(pathParts))
364
+ );
365
+
366
+ return {
367
+ ...routeDict,
368
+ text: i18n.has(routeDict.text) ? i18n.t(routeDict.text) : routeDict.text,
369
+ routeName: routeName as ValidRoutePath,
370
+ scrollResetKey,
371
+ allowed: isRouteAllowed(pathname, operation, orgFeatures),
372
+ hasPhysicalToolbar,
373
+ hasAsideNav,
374
+ };
375
+ }
376
+ }
377
+
378
+ return {
379
+ routeName: '/__unknown',
380
+ scrollResetKey: '',
381
+ allowed: false,
382
+ hasPhysicalToolbar: false,
383
+ hasAsideNav: false,
384
+ } as any;
385
+ }
386
+
387
+ /**
388
+ * Check if this organization's operation allows access to this route path
389
+ * @param pathname - pathname to check.
390
+ * @param operation - Organization operation.
391
+ * @param orgFeatures - Enabled features for organization.
392
+ */
393
+
394
+ export function isRouteAllowed(
395
+ pathname: ValidRoutePath | string,
396
+ operation?: OrganizationOperationEnum | null,
397
+ orgFeatures?: OrganizationFeatureEnum[] | null,
398
+ ): boolean {
399
+
400
+ let routeDict = ROUTES_DICT[pathname as ValidRoutePath];
401
+ if (!routeDict) {
402
+ const parts = pathname.split('/');
403
+ for (let i = parts.length; i > 0; i--) {
404
+ const subPath = parts.slice(0, i).join('/');
405
+
406
+ // @ts-ignore - sub paths may not be valid route paths
407
+ routeDict = ROUTES_DICT[subPath] || ROUTES_DICT[subPath + '/'];
408
+ if (routeDict || i <= 3) {
409
+ break;
410
+ }
411
+ }
412
+ }
413
+
414
+ if (!routeDict) {
415
+ // Assume true if there are no rules set
416
+ return true;
417
+ }
418
+
419
+ return (
420
+ (!routeDict.allowedOperations || routeDict.allowedOperations.includes(operation || '')) &&
421
+ (!routeDict.notAllowedOperations || !routeDict.notAllowedOperations.includes(operation || '')) &&
422
+ // If {orgFeature} is null, assume data is not finished loading yet, and allow "benefit of doubt" access
423
+ (!orgFeatures || !routeDict.requiredFeature || routeDict.requiredFeature.some((feature) => orgFeatures.includes(feature)))
424
+ );
425
+ }
426
+
427
+ /**
428
+ * Helper; because i18n isn't registered when this file opens, we gotta do this
429
+ */
430
+
431
+ function routesDictI18n(routeDictObj: RouteDictObj) {
432
+ return {
433
+ ...routeDictObj,
434
+ text: i18n.t(routeDictObj.text),
435
+ };
436
+ }
437
+
438
+ /**
439
+ * Helper; get static nav list based on organization's operation & features
440
+ */
441
+
442
+ interface NavListItem {
443
+ break: never;
444
+ to: string;
445
+ text: string;
446
+ iconName: string;
447
+ };
448
+
449
+ interface NavBreakItem {
450
+ break: boolean;
451
+ to: never;
452
+ text: never;
453
+ iconName: never;
454
+ };
455
+
456
+ export type NavigationItem = NavListItem | NavBreakItem;
457
+
458
+ export function getNavigationList(
459
+ operation: OrganizationOperationEnum | null,
460
+ orgFeatures?: OrganizationFeatureEnum[],
461
+ ): NavigationItem[] {
462
+
463
+ const breakItem = {
464
+ break: true,
465
+ };
466
+
467
+ let navListArr: {
468
+ text: string;
469
+ initialExpanded?: boolean;
470
+ navList: (RouteDictObj | { break: boolean })[];
471
+ }[] = [];
472
+
473
+ switch (operation) {
474
+ case 'ARABLE':
475
+ navListArr = [
476
+ {
477
+ text: i18n.t('form.reports'),
478
+ navList: [
479
+ ROUTES_DICT['/app/organic'],
480
+ ROUTES_DICT['/app/globalgap/'],
481
+ ]
482
+ },
483
+ {
484
+ text: i18n.t('log.food_safety'),
485
+ navList: [
486
+ ROUTES_DICT['/app/cleaning'],
487
+ // ROUTES_DICT['/app/hygiene'],
488
+ // ROUTES_DICT['/app/sanitation'],
489
+ // ROUTES_DICT['/app/materials'],
490
+ // ROUTES_DICT['/app/biosecurity'],
491
+ // ROUTES_DICT['/app/employees'],
492
+ ]
493
+ },
494
+ {
495
+ text: i18n.t(`org.type_active.${operation}`),
496
+ navList: [
497
+ ROUTES_DICT['/app/seeding'],
498
+ ROUTES_DICT['/app/transplanting'],
499
+ ROUTES_DICT['/app/field-work'],
500
+ ROUTES_DICT['/app/harvested'],
501
+ ROUTES_DICT['/app/post-harvest'],
502
+
503
+ breakItem,
504
+
505
+ ROUTES_DICT['/app/purchases'],
506
+ ROUTES_DICT['/app/orders'],
507
+ ]
508
+ },
509
+ ];
510
+ break;
511
+ case 'LIVESTOCK':
512
+ navListArr = [
513
+ {
514
+ text: i18n.t(`org.type_active.${operation}`),
515
+ navList: [
516
+ ROUTES_DICT['/app/livestock'],
517
+ { ...ROUTES_DICT['/app/purchases'], text: i18n.t('log.supply_purchases') },
518
+ ]
519
+ },
520
+ ];
521
+ break;
522
+ case 'FARMERS_MARKET':
523
+ navListArr = [
524
+ {
525
+ text: i18n.t(`org.type_active.${operation}`),
526
+ navList: [
527
+ ROUTES_DICT['/app/vendors'],
528
+ ROUTES_DICT['/app/markets'],
529
+ ROUTES_DICT['/app/receipts'],
530
+ ]
531
+ },
532
+ ];
533
+ break;
534
+ case 'GROWER_NETWORK':
535
+ navListArr = [
536
+ {
537
+ text: i18n.t(`org.type_active.${operation}`),
538
+ navList: [
539
+ ROUTES_DICT['/app/growers'],
540
+ ROUTES_DICT['/app/foreign-growers'],
541
+ ]
542
+ }
543
+ ];
544
+ break;
545
+ default:
546
+ navListArr = [];
547
+ }
548
+
549
+ // @ts-ignore
550
+ navListArr = [{
551
+ ...ROUTES_DICT['/app'],
552
+ iconName: COMMON_ICON_NAMES[operation!] || 'home',
553
+ },
554
+ breakItem,
555
+ // @ts-ignore
556
+ ].concat(navListArr).concat([{
557
+ text: i18n.t('form.advanced'),
558
+ initialExpanded: false,
559
+ navList: [
560
+ ROUTES_DICT['/app/ai-workflows'],
561
+ ROUTES_DICT['/app/logs']
562
+ ]
563
+ }] as any);
564
+
565
+ const mapNavItems = (item: any) => {
566
+ if (Array.isArray(item)) {
567
+ const [navItem, ...requiredFeatures] = item;
568
+ if (requiredFeatures.some(feature => orgFeatures?.includes?.(feature))) {
569
+ return navItem;
570
+ }
571
+ return null;
572
+ } else if (item?.navList) {
573
+ item.navList = item.navList.reduce((acc: any[], x: any, i: number) => {
574
+ const mappedItem = mapNavItems(x);
575
+ if (
576
+ mappedItem &&
577
+ (mappedItem.to || (acc.length && i !== (item.navList.length - 1)))
578
+ ) {
579
+ if (mappedItem.text && i18n.has(mappedItem.text)) {
580
+ mappedItem.text = i18n.t(mappedItem.text);
581
+ }
582
+ acc.push(mappedItem);
583
+ }
584
+ return acc;
585
+ }, []);
586
+
587
+ if (!item.navList.length) {
588
+ return null;
589
+ }
590
+ } else if (item?.to && !isRouteAllowed(item.to, operation, orgFeatures)) {
591
+ return null;
592
+ }
593
+
594
+ if (item?.text && i18n.has(item.text)) {
595
+ item.text = i18n.t(item.text);
596
+ }
597
+
598
+ return item;
599
+ };
600
+
601
+ return navListArr.map(mapNavItems).filter(Boolean);
602
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,13 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ESNext",
4
+ "module": "ESNext",
5
+ "moduleResolution": "Bundler",
6
+ "allowImportingTsExtensions": true,
7
+ "noEmit": true,
8
+ "strict": true,
9
+ "skipLibCheck": true,
10
+ "esModuleInterop": true
11
+ },
12
+ "include": ["src/**/*"]
13
+ }