swoop-common 2.2.210 → 2.2.211

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.
@@ -2,6 +2,12 @@ import React from "react";
2
2
  import { DTOComponentRead, DTORegionRead, DTOTemplateRead } from "../../api/generated/core/exports";
3
3
  import { PartnerSwoop } from "../../api/generated/swoop/exports";
4
4
  import { ComponentSystem } from "../consts/system";
5
+ export type ComponentLookupMap = Record<string, DTOComponentRead>;
6
+ interface InlineContentDefinition {
7
+ getRequiredComponentIds?: (component: DTOComponentRead, lookups: ComponentLookupMap) => string[];
8
+ render: (component: DTOComponentRead, lookups: ComponentLookupMap, isTemplateEditor: boolean) => React.ReactNode | null;
9
+ }
10
+ export declare const getInlineContentDefinition: (templateId: string | undefined) => InlineContentDefinition | undefined;
5
11
  export interface ComponentListItemProps {
6
12
  component: DTOComponentRead;
7
13
  isSelected: boolean;
@@ -13,6 +19,7 @@ export interface ComponentListItemProps {
13
19
  onSelect: (component: DTOComponentRead) => void;
14
20
  showDivider: boolean;
15
21
  system?: ComponentSystem;
22
+ lookups?: ComponentLookupMap;
16
23
  }
17
24
  declare const ComponentListItem: React.FC<ComponentListItemProps>;
18
25
  export default ComponentListItem;
@@ -1,9 +1,9 @@
1
- import React, { useEffect, useState } from "react";
1
+ import React from "react";
2
2
  import { Box, Button, Chip, Divider, ListItem, ListItemButton, ListItemText, Typography, } from "@mui/material";
3
- import { getComponentDataByTemplateName, TEMPLATE_NAMES } from "../../lib/config/templateStructure";
4
- import { getComponentsByIds } from "../util/api";
3
+ import { getComponentDataByTemplateName, packagesTemplateStructure, templateStructure, TEMPLATE_NAMES } from "../../lib/config/templateStructure";
5
4
  import { COMPONENT_SYSTEMS } from "../consts/system";
6
5
  import { getTemplateChipStyle } from "../consts/templateChipStyles";
6
+ import { convertToTitleCase } from "../util/text";
7
7
  /*
8
8
  Content per item type from ticket:
9
9
  Package: Trip ID (hide on IB), Start city, Private, Guided
@@ -24,51 +24,135 @@ import { getTemplateChipStyle } from "../consts/templateChipStyles";
24
24
 
25
25
  When Private or Guided is false or unpopulated, the field is omitted entirely
26
26
  */
27
- const INLINE_CONTENT_BY_TEMPLATE = {
28
- [TEMPLATE_NAMES.SHIP_ACCOMMODATION]: {
27
+ // Shared by every child of Package (Cruise, Excursions, Guided Multiday Activity, All Inclusive
28
+ // Hotel, Private Tour, Group Tour - see packagesTemplateStructure) - they all surface the same
29
+ // Trip ID / Start City / Private / Guided fields, sourced from the Package componentFields entry
30
+ // that's present on all of them.
31
+ const PACKAGE_CHILD_INLINE_CONTENT = {
32
+ getRequiredComponentIds: (component) => {
33
+ var _a;
34
+ const startCityId = (_a = getComponentDataByTemplateName(TEMPLATE_NAMES.PACKAGE, component.componentFields)) === null || _a === void 0 ? void 0 : _a.startCity;
35
+ return startCityId ? [startCityId] : [];
36
+ },
37
+ render: (component, lookups, isTemplateEditor) => {
38
+ var _a;
39
+ const packageData = getComponentDataByTemplateName(TEMPLATE_NAMES.PACKAGE, component.componentFields);
40
+ const tripId = isTemplateEditor ? component === null || component === void 0 ? void 0 : component.tripId : undefined;
41
+ const isPrivate = packageData === null || packageData === void 0 ? void 0 : packageData.private;
42
+ const guided = packageData === null || packageData === void 0 ? void 0 : packageData.guided;
43
+ const startCity = (packageData === null || packageData === void 0 ? void 0 : packageData.startCity) ? (_a = lookups[packageData.startCity]) === null || _a === void 0 ? void 0 : _a.name : undefined;
44
+ const parts = [
45
+ tripId && `Trip ID: ${tripId}`,
46
+ isPrivate && "Private ✓",
47
+ guided && "Guided ✓",
48
+ startCity && `Start City: ${startCity}`,
49
+ ].filter(Boolean);
50
+ if (!parts.length)
51
+ return null;
52
+ return (React.createElement(Typography, { variant: "body2", color: "text.secondary" }, parts.join(" | ")));
53
+ },
54
+ };
55
+ // Keyed by templateId, not template name/label. Names come back from the API in inconsistent
56
+ // casing (and some, like GuidedMultidayActivity, have no whitespace at all so title-casing can't
57
+ // reliably normalize them) - templateId is the one thing guaranteed to match exactly.
58
+ const INLINE_CONTENT_BY_TEMPLATE = Object.assign(Object.assign({ [templateStructure[TEMPLATE_NAMES.SHIP_ACCOMMODATION]]: {
29
59
  render: (component, _lookups, isTemplateEditor) => {
30
60
  var _a;
31
61
  if (!isTemplateEditor)
32
62
  return null;
33
- const vesselId = (_a = getComponentDataByTemplateName(TEMPLATE_NAMES.SHIP_ACCOMMODATION, component.componentFields)) === null || _a === void 0 ? void 0 : _a.vesselID;
63
+ const vesselId = (_a = getComponentDataByTemplateName(TEMPLATE_NAMES.SHIP_ACCOMMODATION, component.componentFields)) === null || _a === void 0 ? void 0 : _a.vesselId;
34
64
  if (!vesselId)
35
65
  return null;
36
66
  return (React.createElement(Typography, { variant: "body2", color: "text.secondary" },
37
67
  "Vessel ID: ",
38
68
  vesselId));
39
69
  },
40
- },
41
- // plus the rest in from there...
42
- };
43
- const getInlineContentDefinition = (templateName) => templateName ? INLINE_CONTENT_BY_TEMPLATE[templateName] : undefined;
44
- const ComponentListItem = ({ component, isSelected, details, onSelect, showDivider, system, }) => {
45
- var _a, _b, _c, _d, _e;
70
+ } }, Object.fromEntries(Object.values(packagesTemplateStructure).map((templateId) => [templateId, PACKAGE_CHILD_INLINE_CONTENT]))), { [templateStructure[TEMPLATE_NAMES.TRANSFER]]: {
71
+ getRequiredComponentIds: (component, lookups) => {
72
+ var _a;
73
+ const journeyId = (_a = getComponentDataByTemplateName(TEMPLATE_NAMES.TRANSFER, component.componentFields)) === null || _a === void 0 ? void 0 : _a.journey;
74
+ if (!journeyId)
75
+ return [];
76
+ const ids = [journeyId];
77
+ // Journey hasn't resolved yet - come back for start/end Location once it has.
78
+ const journeyComponent = lookups[journeyId];
79
+ if (!journeyComponent)
80
+ return ids;
81
+ const journeyData = getComponentDataByTemplateName(TEMPLATE_NAMES.JOURNEYS, journeyComponent.componentFields);
82
+ if (journeyData === null || journeyData === void 0 ? void 0 : journeyData.startLocation)
83
+ ids.push(journeyData.startLocation);
84
+ if (journeyData === null || journeyData === void 0 ? void 0 : journeyData.endLocation)
85
+ ids.push(journeyData.endLocation);
86
+ return ids;
87
+ },
88
+ render: (component, lookups) => {
89
+ var _a, _b;
90
+ const transferData = getComponentDataByTemplateName(TEMPLATE_NAMES.TRANSFER, component.componentFields);
91
+ const driver = transferData === null || transferData === void 0 ? void 0 : transferData.driver;
92
+ const guided = transferData === null || transferData === void 0 ? void 0 : transferData.guided;
93
+ const type = Array.isArray(transferData === null || transferData === void 0 ? void 0 : transferData.type) ? transferData.type : [];
94
+ const journeyId = transferData === null || transferData === void 0 ? void 0 : transferData.journey;
95
+ const journeyComponent = journeyId ? lookups[journeyId] : undefined;
96
+ const journeyData = journeyComponent
97
+ ? getComponentDataByTemplateName(TEMPLATE_NAMES.JOURNEYS, journeyComponent.componentFields)
98
+ : undefined;
99
+ const startLocation = (journeyData === null || journeyData === void 0 ? void 0 : journeyData.startLocation) ? (_a = lookups[journeyData.startLocation]) === null || _a === void 0 ? void 0 : _a.name : undefined;
100
+ const endLocation = (journeyData === null || journeyData === void 0 ? void 0 : journeyData.endLocation) ? (_b = lookups[journeyData.endLocation]) === null || _b === void 0 ? void 0 : _b.name : undefined;
101
+ const parts = [
102
+ driver && `Driver ✓`,
103
+ guided && "Guided ✓",
104
+ type.length > 0 && `Type: ${type.join(", ")}`,
105
+ startLocation && `Start: ${startLocation}`,
106
+ endLocation && `End: ${endLocation}`,
107
+ ].filter(Boolean);
108
+ if (!parts.length)
109
+ return null;
110
+ return (React.createElement(Typography, { variant: "body2", color: "text.secondary" }, parts.join(" | ")));
111
+ },
112
+ }, [templateStructure[TEMPLATE_NAMES.ACTIVITY]]: {
113
+ getRequiredComponentIds: (component) => {
114
+ const activityData = getComponentDataByTemplateName(TEMPLATE_NAMES.ACTIVITY, component.componentFields);
115
+ return [activityData === null || activityData === void 0 ? void 0 : activityData.startLocation, activityData === null || activityData === void 0 ? void 0 : activityData.endLocation].filter((id) => Boolean(id));
116
+ },
117
+ render: (component, lookups) => {
118
+ var _a, _b;
119
+ const activityData = getComponentDataByTemplateName(TEMPLATE_NAMES.ACTIVITY, component.componentFields);
120
+ const startLocation = (activityData === null || activityData === void 0 ? void 0 : activityData.startLocation) ? (_a = lookups[activityData.startLocation]) === null || _a === void 0 ? void 0 : _a.name : undefined;
121
+ const endLocation = (activityData === null || activityData === void 0 ? void 0 : activityData.endLocation) ? (_b = lookups[activityData.endLocation]) === null || _b === void 0 ? void 0 : _b.name : undefined;
122
+ const type = activityData === null || activityData === void 0 ? void 0 : activityData.type;
123
+ const guided = activityData === null || activityData === void 0 ? void 0 : activityData.guided;
124
+ const parts = [
125
+ startLocation && `Start: ${startLocation}`,
126
+ endLocation && `End: ${endLocation}`,
127
+ type && `Type: ${type}`,
128
+ guided && "Guided ✓",
129
+ ].filter(Boolean);
130
+ if (!parts.length)
131
+ return null;
132
+ return (React.createElement(Typography, { variant: "body2", color: "text.secondary" }, parts.join(" | ")));
133
+ },
134
+ }, [templateStructure[TEMPLATE_NAMES.CRUISE_ACTIVITY]]: {
135
+ render: (component) => {
136
+ var _a;
137
+ const type = (_a = getComponentDataByTemplateName(TEMPLATE_NAMES.CRUISE_ACTIVITY, component.componentFields)) === null || _a === void 0 ? void 0 : _a.type;
138
+ if (!type)
139
+ return null;
140
+ return (React.createElement(Typography, { variant: "body2", color: "text.secondary" },
141
+ "Type: ",
142
+ type));
143
+ },
144
+ } });
145
+ export const getInlineContentDefinition = (templateId) => templateId ? INLINE_CONTENT_BY_TEMPLATE[templateId] : undefined;
146
+ const ComponentListItem = ({ component, isSelected, details, onSelect, showDivider, system, lookups, }) => {
147
+ var _a, _b, _c;
46
148
  const activityData = (_a = getComponentDataByTemplateName(TEMPLATE_NAMES.CRUISE_ACTIVITY, component.componentFields)) !== null && _a !== void 0 ? _a : getComponentDataByTemplateName(TEMPLATE_NAMES.ACTIVITY, component.componentFields);
47
149
  const displayName = (activityData === null || activityData === void 0 ? void 0 : activityData.type)
48
150
  ? `${activityData.type} - ${component.name}`
49
151
  : component.name;
50
- const convertToTitleCase = (str) => {
51
- return str.replace(/\w\S*/g, (txt) => txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase());
52
- };
53
- const inlineContentDefinition = getInlineContentDefinition((_b = details.template) === null || _b === void 0 ? void 0 : _b.name);
54
- const requiredComponentIds = (_d = (_c = inlineContentDefinition === null || inlineContentDefinition === void 0 ? void 0 : inlineContentDefinition.getRequiredComponentIds) === null || _c === void 0 ? void 0 : _c.call(inlineContentDefinition, component)) !== null && _d !== void 0 ? _d : [];
55
- const requiredComponentIdsKey = requiredComponentIds.join(",");
56
- const [lookups, setLookups] = useState({});
152
+ const templateName = ((_b = details.template) === null || _b === void 0 ? void 0 : _b.name) ? convertToTitleCase(details.template.name) : undefined;
153
+ const inlineContentDefinition = getInlineContentDefinition(component.templateId);
57
154
  const isTemplateEditor = system === COMPONENT_SYSTEMS.TEMPLATE_EDITOR;
58
- useEffect(() => {
59
- const missingIds = requiredComponentIds.filter((id) => !lookups[id]);
60
- if (!missingIds.length)
61
- return;
62
- let cancelled = false;
63
- getComponentsByIds(missingIds).then((fetched) => {
64
- if (cancelled)
65
- return;
66
- setLookups((prev) => (Object.assign(Object.assign({}, prev), Object.fromEntries(fetched.map((c) => [c.id, c])))));
67
- });
68
- return () => { cancelled = true; };
69
- // eslint-disable-next-line react-hooks/exhaustive-deps
70
- }, [requiredComponentIdsKey]);
71
- const inlineContent = (_e = inlineContentDefinition === null || inlineContentDefinition === void 0 ? void 0 : inlineContentDefinition.render(component, lookups, isTemplateEditor)) !== null && _e !== void 0 ? _e : null;
155
+ const inlineContent = (_c = inlineContentDefinition === null || inlineContentDefinition === void 0 ? void 0 : inlineContentDefinition.render(component, lookups !== null && lookups !== void 0 ? lookups : {}, isTemplateEditor)) !== null && _c !== void 0 ? _c : null;
72
156
  return (React.createElement(React.Fragment, null,
73
157
  React.createElement(ListItem, { disablePadding: true },
74
158
  React.createElement(ListItemButton, { onClick: () => onSelect(component), selected: isSelected, sx: {
@@ -84,7 +168,7 @@ const ComponentListItem = ({ component, isSelected, details, onSelect, showDivid
84
168
  React.createElement(Box, { width: "100%", display: "flex", justifyContent: "space-between", alignItems: "flex-start", gap: 2 },
85
169
  React.createElement(ListItemText, { primary: React.createElement(Box, { display: "flex", flexDirection: "column", gap: 0.5 },
86
170
  details.template && (React.createElement(Box, null,
87
- React.createElement(Chip, { label: convertToTitleCase(details.template.name), size: "small", sx: Object.assign(Object.assign({}, getTemplateChipStyle(details.template.name)), { fontWeight: 500 }) }))),
171
+ React.createElement(Chip, { label: templateName, size: "small", style: getTemplateChipStyle(templateName || ""), sx: { fontWeight: 500 } }))),
88
172
  React.createElement(Typography, { variant: "subtitle1", fontWeight: "medium" }, displayName)), secondary: React.createElement(Box, { display: "flex", flexDirection: "column", gap: 0.5, mt: 1 },
89
173
  React.createElement(Box, { display: "flex", alignItems: "center", gap: 3, flexWrap: "wrap" },
90
174
  details.partners.length > 0 && (React.createElement(Typography, { variant: "body2", color: "text.secondary" },
@@ -11,9 +11,9 @@ import React, { useState, useEffect, useMemo } from "react";
11
11
  import { Dialog, DialogTitle, DialogContent, DialogActions, Button, TextField, MenuItem, Box, List, Typography, InputAdornment, IconButton, CircularProgress, Pagination, Autocomplete, Checkbox, FormControlLabel, } from "@mui/material";
12
12
  import { Search as SearchIcon, Clear as ClearIcon, FilterList as FilterIcon, } from "@mui/icons-material";
13
13
  import { PARTNER_REGION_READABLE } from "../consts/region";
14
- import { fetchComponents, getAllPartners } from "../util/api";
14
+ import { fetchComponents, getAllPartners, getComponentsByIds } from "../util/api";
15
15
  import { InternalServices } from "../../api/init";
16
- import ComponentListItem from "./ComponentListItem";
16
+ import ComponentListItem, { getInlineContentDefinition } from "./ComponentListItem";
17
17
  import { TEMPLATE_NAMES, templateStructure, } from "../../lib/config/templateStructure";
18
18
  export default function ComponentPickerModal({ open, onClose, onSelect, selectedComponent, parentIdsFilter, system, }) {
19
19
  const [searchTerm, setSearchTerm] = useState("");
@@ -24,6 +24,7 @@ export default function ComponentPickerModal({ open, onClose, onSelect, selected
24
24
  const [templates, setTemplates] = useState([]);
25
25
  const [partners, setPartners] = useState([]);
26
26
  const [regions, setRegions] = useState([]);
27
+ const [lookups, setLookups] = useState({});
27
28
  const [loading, setLoading] = useState(false);
28
29
  const [initialLoading, setInitialLoading] = useState(false);
29
30
  const [page, setPage] = useState(1);
@@ -74,6 +75,27 @@ export default function ComponentPickerModal({ open, onClose, onSelect, selected
74
75
  citiesOnly,
75
76
  page,
76
77
  ]);
78
+ useEffect(() => {
79
+ // Depends on `lookups` so multi-hop chains (Transfer -> Journey -> start/end Location)
80
+ // converge: each round's fetch feeds getRequiredComponentIds the next hop's ids, until a
81
+ // round finds nothing missing and this becomes a no-op.
82
+ const idsNeeded = new Set();
83
+ components.forEach((component) => {
84
+ var _a, _b, _c;
85
+ const requiredIds = (_c = (_b = (_a = getInlineContentDefinition(component.templateId)) === null || _a === void 0 ? void 0 : _a.getRequiredComponentIds) === null || _b === void 0 ? void 0 : _b.call(_a, component, lookups)) !== null && _c !== void 0 ? _c : [];
86
+ requiredIds.forEach((id) => idsNeeded.add(id));
87
+ });
88
+ const missingIds = Array.from(idsNeeded).filter((id) => !lookups[id]);
89
+ if (!missingIds.length)
90
+ return;
91
+ let cancelled = false;
92
+ getComponentsByIds(missingIds).then((fetched) => {
93
+ if (cancelled)
94
+ return;
95
+ setLookups((prev) => (Object.assign(Object.assign({}, prev), Object.fromEntries(fetched.map((c) => [c.id, c])))));
96
+ });
97
+ return () => { cancelled = true; };
98
+ }, [components, lookups]);
77
99
  useEffect(() => {
78
100
  if (open) {
79
101
  setPage(1);
@@ -240,7 +262,7 @@ export default function ComponentPickerModal({ open, onClose, onSelect, selected
240
262
  React.createElement(Typography, { variant: "body1", color: "text.secondary" }, "Loading components..."))) : components.length === 0 ? (React.createElement(Box, { display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", py: 4 },
241
263
  React.createElement(FilterIcon, { sx: { fontSize: 48, color: "text.secondary", mb: 2 } }),
242
264
  React.createElement(Typography, { variant: "body1", color: "text.secondary" }, "No components match your search criteria"),
243
- React.createElement(Typography, { variant: "body2", color: "text.secondary" }, "Try adjusting your filters or search term"))) : (components.map((component, index) => (React.createElement(ComponentListItem, { key: component.id, component: component, isSelected: (selectedComponent === null || selectedComponent === void 0 ? void 0 : selectedComponent.id) === component.id, details: getComponentDetails(component), onSelect: handleSelect, showDivider: index < components.length - 1, system: system }))))),
265
+ React.createElement(Typography, { variant: "body2", color: "text.secondary" }, "Try adjusting your filters or search term"))) : (components.map((component, index) => (React.createElement(ComponentListItem, { key: component.id, component: component, isSelected: (selectedComponent === null || selectedComponent === void 0 ? void 0 : selectedComponent.id) === component.id, details: getComponentDetails(component), onSelect: handleSelect, showDivider: index < components.length - 1, system: system, lookups: lookups }))))),
244
266
  components.length > 0 && (React.createElement(Box, { display: "flex", justifyContent: "center", mt: 2 },
245
267
  React.createElement(Pagination, { count: Math.ceil(totalCount / pageSize), page: page, onChange: (_, newPage) => setPage(newPage), color: "primary" })))),
246
268
  React.createElement(DialogActions, null,
@@ -21,4 +21,4 @@ export const TEMPLATE_CHIP_STYLES = {
21
21
  [TEMPLATE_NAMES.ROOM]: { backgroundColor: "#FCE9DA", color: "#B5651D" }, // peach
22
22
  [TEMPLATE_NAMES.GROUP_TOUR]: { backgroundColor: "#F0E6FB", color: "#7A3FA0" }, // violet
23
23
  };
24
- export const getTemplateChipStyle = (templateName) => { var _a; return (_a = TEMPLATE_CHIP_STYLES[templateName]) !== null && _a !== void 0 ? _a : { backgroundColor: "grey.200", color: "text.secondary" }; };
24
+ export const getTemplateChipStyle = (templateName) => { var _a; return (_a = TEMPLATE_CHIP_STYLES[templateName]) !== null && _a !== void 0 ? _a : { backgroundColor: "#EEEEEE", color: "#616161" }; };
@@ -0,0 +1 @@
1
+ export declare const convertToTitleCase: (str: string) => string;
@@ -0,0 +1 @@
1
+ export const convertToTitleCase = (str) => str.replace(/\w\S*/g, (txt) => txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase());
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "swoop-common",
3
- "version": "2.2.210",
3
+ "version": "2.2.211",
4
4
  "main": "dist/api/index.js",
5
5
  "types": "dist/api/index.d.ts",
6
6
  "exports": {