@riosst100/pwa-marketplace 2.9.5 → 2.9.7

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,112 @@
1
+ import { useCallback, useState, useMemo } from 'react';
2
+ import { useQuery } from '@apollo/client';
3
+
4
+ import mergeOperations from '@magento/peregrine/lib/util/shallowMerge';
5
+ import DEFAULT_OPERATIONS from './orderRow.gql';
6
+
7
+ /**
8
+ * Safe override of useOrderRow to guard against missing product data.
9
+ * Fixes: TypeError Cannot read properties of undefined (reading 'variants').
10
+ */
11
+ export const useOrderRow = props => {
12
+ const { items = [], operations: ops } = props;
13
+ const operations = mergeOperations(DEFAULT_OPERATIONS, ops);
14
+ const { getProductThumbnailsQuery, getConfigurableThumbnailSource } = operations;
15
+
16
+ const urlKeys = useMemo(() => {
17
+ return items.map(item => item.product_url_key).filter(Boolean).sort();
18
+ }, [items]);
19
+
20
+ const { data, loading } = useQuery(getProductThumbnailsQuery, {
21
+ fetchPolicy: 'cache-and-network',
22
+ nextFetchPolicy: 'cache-first',
23
+ variables: { urlKeys }
24
+ });
25
+
26
+ const { data: configurableThumbnailSourceData } = useQuery(
27
+ getConfigurableThumbnailSource,
28
+ { fetchPolicy: 'cache-and-network' }
29
+ );
30
+
31
+ const configurableThumbnailSource = useMemo(() => {
32
+ if (configurableThumbnailSourceData && configurableThumbnailSourceData.storeConfig) {
33
+ return configurableThumbnailSourceData.storeConfig.configurable_thumbnail_source;
34
+ }
35
+ return undefined;
36
+ }, [configurableThumbnailSourceData]);
37
+
38
+ const imagesData = useMemo(() => {
39
+ const mappedImagesData = {};
40
+
41
+ if (!data || !data.products || !Array.isArray(data.products.items)) {
42
+ return mappedImagesData;
43
+ }
44
+
45
+ items.forEach(item => {
46
+ if (!item) return;
47
+
48
+ const product = data.products.items.find(
49
+ element => item.product_url_key === element.url_key
50
+ );
51
+
52
+ if (!product) {
53
+ // No matching product found for url_key; leave null to avoid crashes upstream.
54
+ mappedImagesData[item.product_sku] = null;
55
+ return;
56
+ }
57
+
58
+ // When using variant thumbnail, prefer the configured variant image if present.
59
+ if (
60
+ configurableThumbnailSource === 'itself' &&
61
+ Array.isArray(product.variants) &&
62
+ product.variants.length > 0
63
+ ) {
64
+ const foundVariant = product.variants.find(
65
+ variant => variant && variant.product && variant.product.sku === item.product_sku
66
+ );
67
+ mappedImagesData[item.product_sku] = (foundVariant && foundVariant.product) || product;
68
+ } else {
69
+ mappedImagesData[item.product_sku] = product;
70
+ }
71
+ });
72
+
73
+ return mappedImagesData;
74
+ }, [data, items, configurableThumbnailSource]);
75
+
76
+ const [isOpen, setIsOpen] = useState(false);
77
+ const handleContentToggle = useCallback(() => {
78
+ setIsOpen(currentValue => !currentValue);
79
+ }, []);
80
+
81
+ return {
82
+ loading,
83
+ imagesData,
84
+ isOpen,
85
+ handleContentToggle
86
+ };
87
+ };
88
+
89
+ /**
90
+ * JSDoc type definitions
91
+ */
92
+
93
+ /**
94
+ * GraphQL operations for the Order Row Component
95
+ *
96
+ * @typedef {Object} OrderRowOperations
97
+ *
98
+ * @property {GraphQLAST} getProductThumbnailsQuery The query used to get product thumbnails of items in the Order.
99
+ *
100
+ * @see [`orderRow.gql.js`]{@link https://github.com/magento/pwa-studio/blob/develop/packages/venia-ui/lib/components/OrderHistoryPage/orderRow.gql.js}
101
+ * for queries used in Venia
102
+ */
103
+
104
+ /**
105
+ * Props data to use when rendering a collapsed image gallery
106
+ *
107
+ * @typedef {Object} OrderRowTalonProps
108
+ *
109
+ * @property {Object} imagesData Images data with thumbnail URLs to render.
110
+ * @property {Boolean} isOpen Boolean which represents if a row is open or not
111
+ * @property {Function} handleContentToggle Callback to toggle isOpen value
112
+ */
@@ -0,0 +1,202 @@
1
+ import { gql } from '@apollo/client';
2
+
3
+ const LofmpRmasFragment = gql`
4
+ fragment LofmpRmasFragment on RmaSearchResult {
5
+ items {
6
+ created_at
7
+ increment_id
8
+ items {
9
+ condition
10
+ id
11
+ image_url
12
+ name
13
+ price {
14
+ currency
15
+ value
16
+ }
17
+ qty_requested
18
+ reason
19
+ resolution
20
+ seller_name
21
+ sku
22
+ }
23
+ order {
24
+ created_at
25
+ id
26
+ increment_id
27
+ status
28
+ status_label
29
+ }
30
+ order_increment_id
31
+ rma_date
32
+ rma_id
33
+ seller {
34
+ id
35
+ name
36
+ url
37
+ }
38
+ status
39
+ status_label
40
+ }
41
+ total_count
42
+ }
43
+ `;
44
+
45
+ export const LOFMP_SEND_RMA_MESSAGE = gql`
46
+ mutation lofmpSendRmaMessage($input: SendMessageInput!) {
47
+ lofmpSendRmaMessage(input: $input) {
48
+ created_message {
49
+ attachments {
50
+ id
51
+ name
52
+ url
53
+ }
54
+ created_at
55
+ id
56
+ sender_email
57
+ sender_name
58
+ text
59
+ }
60
+ message
61
+ success
62
+ }
63
+ }
64
+ `;
65
+
66
+ export const LOFMP_RMA_SHIPPING_CONFIRM = gql`
67
+ mutation lofmpRmaShippingConfirm($rma_id: Int!) {
68
+ lofmpRmaShippingConfirm(rma_id: $rma_id) {
69
+ message
70
+ success
71
+ }
72
+ }
73
+ `;
74
+
75
+ export const GET_LOFMP_RMA_CONFIGURATIONS = gql`
76
+ query GetLofmpRmaConfigurations {
77
+ lofmpRmaConfigurations {
78
+ conditions {
79
+ id
80
+ name
81
+ }
82
+ reasons {
83
+ id
84
+ name
85
+ }
86
+ resolutions {
87
+ id
88
+ name
89
+ }
90
+ }
91
+ }
92
+ `;
93
+
94
+ export const GET_LOFMP_RMAS = gql`
95
+ query GetLofmpRmas($filter: RmaFilterInput, $pageSize: Int!, $currentPage: Int) {
96
+ lofmpRmas(filter: $filter, pageSize: $pageSize, currentPage: $currentPage) {
97
+ ...LofmpRmasFragment
98
+ }
99
+ }
100
+ ${LofmpRmasFragment}
101
+ `;
102
+
103
+ export const LOFMP_CREATE_RMA = gql`
104
+ mutation lofmpCreateRma($input: CreateRmaInput!) {
105
+ lofmpCreateRma(input: $input) {
106
+ message
107
+ rma_id
108
+ status_id
109
+ success
110
+ }
111
+ }
112
+ `;
113
+
114
+ export const GET_LOFMP_RMA_DETAIL = gql`
115
+ query GetLofmpRmaDetail($rmaId: ID!) {
116
+ lofmpRmaDetail(rmaId: $rmaId) {
117
+ created_at
118
+ increment_id
119
+ items {
120
+ condition
121
+ id
122
+ image_url
123
+ name
124
+ product_options {
125
+ label
126
+ value
127
+ }
128
+ qty_requested
129
+ reason
130
+ resolution
131
+ sku
132
+ }
133
+ messages {
134
+ attachments {
135
+ id
136
+ name
137
+ url
138
+ }
139
+ created_at
140
+ id
141
+ sender_email
142
+ sender_name
143
+ text
144
+ }
145
+ order {
146
+ created_at
147
+ id
148
+ increment_id
149
+ status
150
+ status_label
151
+ }
152
+ order_increment_id
153
+ rma_date
154
+ rma_id
155
+ seller {
156
+ id
157
+ name
158
+ url
159
+ }
160
+ shipping_address {
161
+ address_type
162
+ base_cash_on_delivery
163
+ cash_on_delivery
164
+ city
165
+ company
166
+ country_id
167
+ customer_address_id
168
+ customer_id
169
+ email
170
+ entity_id
171
+ fax
172
+ firstname
173
+ lastname
174
+ middlename
175
+ parent_id
176
+ postcode
177
+ prefix
178
+ quote_address_id
179
+ region
180
+ region_id
181
+ street
182
+ suffix
183
+ telephone
184
+ vat_id
185
+ vat_is_valid
186
+ vat_request_date
187
+ vat_request_id
188
+ vat_request_success
189
+ }
190
+ status
191
+ status_label
192
+ }
193
+ }
194
+ `;
195
+
196
+ export default {
197
+ getLofmpRmasQuery: GET_LOFMP_RMAS,
198
+ lofmpCreateRmaMutation: LOFMP_CREATE_RMA,
199
+ getLofmpRmaDetailQuery: GET_LOFMP_RMA_DETAIL,
200
+ lofmpRmaShippingConfirmMutation: LOFMP_RMA_SHIPPING_CONFIRM,
201
+ lofmpSendRmaMessageMutation: LOFMP_SEND_RMA_MESSAGE
202
+ };
@@ -12,13 +12,14 @@ import OrderProgressBar from './orderProgressBar';
12
12
  import OrderDetails from './OrderDetails';
13
13
  import defaultClasses from './orderRow.module.css';
14
14
  import cn from 'classnames';
15
- import { Verify, Shop } from 'iconsax-react';
15
+ import { Verify, Shop, ConvertCard } from 'iconsax-react';
16
16
  import PlaceholderImage from '@magento/venia-ui/lib/components/Image/placeholderImage';
17
- import { Link } from 'react-router-dom';
17
+ import { Link, useHistory } from 'react-router-dom';
18
18
 
19
19
  const OrderRow = props => {
20
20
  const { order } = props;
21
21
  const { formatMessage } = useIntl();
22
+ const history = useHistory();
22
23
  const {
23
24
  invoices,
24
25
  items,
@@ -109,24 +110,75 @@ const OrderRow = props => {
109
110
  width: 50
110
111
  };
111
112
 
112
- // Helper to resolve a product image by SKU with graceful fallback
113
- const getThumbnailForSku = (sku, fallbackLabel) => {
113
+
114
+ // Helper to resolve a product image by SKU with graceful fallback, now prefers product_image_url from order.items
115
+ const getThumbnailForSku = (sku, fallbackLabel, idx) => {
114
116
  let url = null;
115
117
  let label = fallbackLabel;
116
- if (imagesData && typeof imagesData === 'object') {
118
+ // Prefer product_image_url from order.items if present and is a non-empty string
119
+ if (items && items[idx] && typeof items[idx].product_image_url === 'string' && items[idx].product_image_url.trim() !== '') {
120
+ url = items[idx].product_image_url;
121
+ } else if (imagesData && typeof imagesData === 'object') {
117
122
  const byKey = sku && imagesData[sku];
118
123
  const byValue = !byKey && sku
119
124
  ? Object.values(imagesData).find(entry => entry?.sku === sku)
120
125
  : null;
121
126
  const chosen = byKey || byValue;
122
- if (chosen?.thumbnail?.url) {
127
+ if (chosen?.thumbnail?.url && typeof chosen.thumbnail.url === 'string' && chosen.thumbnail.url.trim() !== '') {
123
128
  url = chosen.thumbnail.url;
124
129
  label = chosen.thumbnail.label || label;
125
130
  }
126
131
  }
132
+ // If url is not a non-empty string, fallback to null (will trigger placeholder)
133
+ if (typeof url !== 'string' || url.trim() === '') {
134
+ url = null;
135
+ }
127
136
  return { url, label };
128
137
  };
129
138
 
139
+ const showNewReturnButton = status === 'Complete' || derivedStatus === formatMessage({ id: 'orderRow.deliveredText', defaultMessage: 'Delivered' });
140
+
141
+ const handleNewReturn = () => {
142
+ // Inject product_image per item with safe fallbacks (prefer product_image_url from query)
143
+ let orderWithImages = { ...order };
144
+ if (orderWithImages.items && Array.isArray(orderWithImages.items)) {
145
+ orderWithImages = {
146
+ ...orderWithImages,
147
+ items: orderWithImages.items.map(it => {
148
+ // Start with empty string to avoid undefined passed to Image component downstream
149
+ let product_image = '';
150
+ // 1) Prefer product_image_url from order items (added by query)
151
+ if (typeof it.product_image_url === 'string' && it.product_image_url.trim() !== '') {
152
+ product_image = it.product_image_url;
153
+ }
154
+ // 2) Fallback to imagesData thumbnail url (if available)
155
+ else if (
156
+ imagesData &&
157
+ it.product_sku &&
158
+ imagesData[it.product_sku]?.thumbnail?.url &&
159
+ typeof imagesData[it.product_sku].thumbnail.url === 'string' &&
160
+ imagesData[it.product_sku].thumbnail.url.trim() !== ''
161
+ ) {
162
+ product_image = imagesData[it.product_sku].thumbnail.url;
163
+ }
164
+
165
+ return {
166
+ ...it,
167
+ // Keep product_image_url as-is, add product_image for RMACreate consumers
168
+ product_image
169
+ };
170
+ })
171
+ };
172
+ }
173
+ try {
174
+ localStorage.setItem('rma_order', JSON.stringify(orderWithImages));
175
+ } catch (e) {}
176
+ history.push({
177
+ pathname: `/return/create/${orderNumber}`,
178
+ state: { order: orderWithImages }
179
+ });
180
+ };
181
+
130
182
  return (
131
183
  <li className={classes.root}>
132
184
  <div className='flex flex-col md_flex-row md_items-start justify-between mb-2.5'>
@@ -151,30 +203,25 @@ const OrderRow = props => {
151
203
  </div>
152
204
  </div>
153
205
  <div className='flex flex-col md_flex-row justify-between mb-2.5'>
154
- <div className='flex flex-col ml-[14px]'>
155
- <div className='flex flex-row'>
156
- <div className={cn('mr-4 flex flex-col gap-8')}>
157
- {items && items.length > 0 && items.map((it, idx) => {
158
- const { url, label } = getThumbnailForSku(it.product_sku, it.product_name);
159
- return (
160
- <div key={it.id || idx} className={classes.productImage}>
161
- {url ? (
162
- <img
163
- src={url}
164
- alt={label}
165
- width={60}
166
- className={classes.thumbnail}
167
- />
168
- ) : (
169
- <PlaceholderImage alt={it.product_name} classes={{ root: classes.thumbnail }} width={60} />
170
- )}
171
- </div>
172
- );
173
- })}
174
- </div>
175
- <div className='flex flex-col max-w-[375px] gap-8'>
176
- {items && items.length > 0 && items.map((it, idx) => (
177
- <div key={it.id || idx} className='flex flex-col gap-1 pb-2 last_pb-0'>
206
+ <div className='flex flex-col ml-[5px] gap-y-[20px]'>
207
+ {/* Per-item horizontal flex: image + detail */}
208
+ {items && items.length > 0 && items.map((it, idx) => {
209
+ const { url, label } = getThumbnailForSku(it.product_sku, it.product_name, idx);
210
+ return (
211
+ <div key={it.id || idx} className='flex flex-row gap-4 mb-2'>
212
+ <div className={classes.productImage}>
213
+ {url ? (
214
+ <img
215
+ src={url}
216
+ alt={label}
217
+ width={80}
218
+ className={classes.thumbnail}
219
+ />
220
+ ) : (
221
+ <PlaceholderImage alt={it.product_name} classes={{ root: classes.thumbnail }} width={60} />
222
+ )}
223
+ </div>
224
+ <div className='flex flex-col gap-1 pb-2 last_pb-0 max-w-[375px]'>
178
225
  <div className={cn(classes.productName, 'text-[14] font-medium')}>
179
226
  <span>{it.product_name}</span>
180
227
  </div>
@@ -186,9 +233,9 @@ const OrderRow = props => {
186
233
  </span>
187
234
  </div>
188
235
  </div>
189
- ))}
190
- </div>
191
- </div>
236
+ </div>
237
+ );
238
+ })}
192
239
  </div>
193
240
  {/* Right column: bottom-aligned Order Total + CTA */}
194
241
  <div className='flex flex-col items-end gap-2 md_pl-10 md_self-end mr-4 mb-1'>
@@ -201,19 +248,37 @@ const OrderRow = props => {
201
248
  </span>
202
249
  <div className="text-lg font-medium">{orderTotalPrice}</div>
203
250
  </div>
204
- <Link
205
- to={{
206
- pathname: `/order-history/view/${orderNumber}`,
207
- state: { order }
208
- }}
209
- >
210
- <span className="bg-blue-700 hover:bg-white hover:text-blue-700 hover:border hover:border-blue-700 rounded-full px-[30px] py-[5px] text-[13px] font-medium text-white transition-all duration-300 ease-in-out">
211
- <FormattedMessage
212
- id={'orderRow.ViewTransactionDetail'}
213
- defaultMessage={'View Order Detail'}
214
- />
215
- </span>
216
- </Link>
251
+ <div className="flex flex-row gap-2 w-full justify-end items-center">
252
+ <Link
253
+ to={{
254
+ pathname: `/order-history/view/${orderNumber}`,
255
+ state: { order }
256
+ }}
257
+ >
258
+ <span className="bg-blue-700 hover:bg-white hover:text-blue-700 hover:border hover:border-blue-700 rounded-full px-[30px] py-[5px] text-[13px] font-medium text-white transition-all duration-300 ease-in-out">
259
+ <FormattedMessage
260
+ id={'orderRow.ViewTransactionDetail'}
261
+ defaultMessage={'View Order Detail'}
262
+ />
263
+ </span>
264
+ </Link>
265
+ {showNewReturnButton && (
266
+ <div
267
+ className={cn(
268
+ "cursor-pointer border border-blue-700 bg-white text-blue-700 hover:bg-blue-50 hover:text-blue-700 hover:border-blue-700",
269
+ "rounded-full px-[10px] py-[3px] text-[13px] font-medium transition-all duration-300 ease-in-out flex items-center gap-2"
270
+ )}
271
+ onClick={handleNewReturn}
272
+ title={formatMessage({ id: 'orderRow.returnProduct', defaultMessage: 'Return Product' })}
273
+ >
274
+ <ConvertCard size={20} color="#f26313" />
275
+ <FormattedMessage
276
+ id={'orderRow.ReturnItems'}
277
+ defaultMessage={'Return Items'}
278
+ />
279
+ </div>
280
+ )}
281
+ </div>
217
282
  </div>
218
283
  </div>
219
284