@vendure/dashboard 3.6.4-master-202605280309 → 3.6.4
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 +3 -3
- package/src/app/common/duplicate-entity-dialog.tsx +2 -1
- package/src/app/routes/_authenticated/_orders/components/fulfill-order-dialog.tsx +2 -1
- package/src/app/routes/_authenticated/_products/components/generate-variants-panel.tsx +133 -32
- package/src/i18n/locales/ar.po +97 -78
- package/src/i18n/locales/bg.po +97 -78
- package/src/i18n/locales/cs.po +97 -78
- package/src/i18n/locales/de.po +97 -78
- package/src/i18n/locales/en.po +97 -78
- package/src/i18n/locales/es.po +97 -78
- package/src/i18n/locales/fa.po +97 -78
- package/src/i18n/locales/fr.po +97 -78
- package/src/i18n/locales/he.po +97 -78
- package/src/i18n/locales/hr.po +97 -78
- package/src/i18n/locales/hu.po +97 -78
- package/src/i18n/locales/it.po +97 -78
- package/src/i18n/locales/ja.po +97 -78
- package/src/i18n/locales/nb.po +97 -78
- package/src/i18n/locales/ne.po +97 -78
- package/src/i18n/locales/nl.po +97 -78
- package/src/i18n/locales/pl.po +97 -78
- package/src/i18n/locales/pt_BR.po +97 -78
- package/src/i18n/locales/pt_PT.po +97 -78
- package/src/i18n/locales/ro.po +97 -78
- package/src/i18n/locales/ru.po +97 -78
- package/src/i18n/locales/sv.po +97 -78
- package/src/i18n/locales/tr.po +97 -78
- package/src/i18n/locales/uk.po +97 -78
- package/src/i18n/locales/zh_Hans.po +97 -78
- package/src/i18n/locales/zh_Hant.po +97 -78
- package/src/lib/components/data-input/affixed-input.tsx +2 -0
- package/src/lib/components/data-input/default-relation-input.tsx +60 -0
- package/src/lib/components/data-input/select-with-options.tsx +12 -5
- package/src/lib/components/shared/configurable-operation-multi-selector.tsx +2 -1
- package/src/lib/components/shared/configurable-operation-selector.tsx +2 -1
- package/src/lib/components/shared/configurable-operation-utils.spec.ts +49 -0
- package/src/lib/components/shared/configurable-operation-utils.ts +18 -0
- package/src/lib/framework/form-engine/form-schema-tools.spec.ts +39 -0
- package/src/lib/framework/form-engine/form-schema-tools.ts +72 -2
- package/src/lib/framework/form-engine/use-generated-form.tsx +13 -10
- package/src/lib/framework/form-engine/utils.spec.ts +50 -0
- package/src/lib/framework/form-engine/utils.ts +14 -0
- package/src/lib/lib/utils.spec.ts +253 -0
- package/src/lib/lib/utils.ts +40 -9
- package/src/lib/providers/auth.tsx +52 -13
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { removeReadonlyAndLocalizedCustomFields } from './utils.js';
|
|
4
|
+
|
|
5
|
+
describe('removeReadonlyAndLocalizedCustomFields', () => {
|
|
6
|
+
it('should return values unchanged when no customFields present', () => {
|
|
7
|
+
const values = { name: 'Test' };
|
|
8
|
+
const result = removeReadonlyAndLocalizedCustomFields(values, []);
|
|
9
|
+
expect(result).toEqual({ name: 'Test' });
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('should return falsy values as-is', () => {
|
|
13
|
+
const result = removeReadonlyAndLocalizedCustomFields(null as any, []);
|
|
14
|
+
expect(result).toBeNull();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('should remove readonly custom fields', () => {
|
|
18
|
+
const values = {
|
|
19
|
+
customFields: {
|
|
20
|
+
editable: 'yes',
|
|
21
|
+
locked: 'no',
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
const configs = [
|
|
25
|
+
{ name: 'editable', type: 'string' },
|
|
26
|
+
{ name: 'locked', type: 'string', readonly: true },
|
|
27
|
+
];
|
|
28
|
+
const result = removeReadonlyAndLocalizedCustomFields(values, configs);
|
|
29
|
+
expect(result.customFields).toEqual({ editable: 'yes' });
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('should remove localeString and localeText fields from root customFields', () => {
|
|
33
|
+
const values = {
|
|
34
|
+
customFields: {
|
|
35
|
+
regularField: 'value',
|
|
36
|
+
localeName: 'should be removed',
|
|
37
|
+
localeDescription: 'should be removed',
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
const configs = [
|
|
41
|
+
{ name: 'regularField', type: 'string' },
|
|
42
|
+
{ name: 'localeName', type: 'localeString' },
|
|
43
|
+
{ name: 'localeDescription', type: 'localeText' },
|
|
44
|
+
];
|
|
45
|
+
const result = removeReadonlyAndLocalizedCustomFields(values, configs);
|
|
46
|
+
expect(result.customFields).toEqual({ regularField: 'value' });
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('should remove non-permitted custom fields (fields not in config)', () => {
|
|
50
|
+
const values = {
|
|
51
|
+
customFields: {
|
|
52
|
+
allowed: 'yes',
|
|
53
|
+
superAdminOnly: 'secret',
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
// Only 'allowed' is in the config (user has permission for it)
|
|
57
|
+
const configs = [{ name: 'allowed', type: 'string' }];
|
|
58
|
+
const result = removeReadonlyAndLocalizedCustomFields(values, configs);
|
|
59
|
+
expect(result.customFields).toEqual({ allowed: 'yes' });
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('should strip all custom fields when config is empty (no permitted fields)', () => {
|
|
63
|
+
const values = {
|
|
64
|
+
customFields: {
|
|
65
|
+
field1: 'a',
|
|
66
|
+
field2: 'b',
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
const result = removeReadonlyAndLocalizedCustomFields(values, []);
|
|
70
|
+
expect(result.customFields).toEqual({});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('should handle relation fields with Id suffix for permitted fields', () => {
|
|
74
|
+
const values = {
|
|
75
|
+
customFields: {
|
|
76
|
+
featuredProductId: '123',
|
|
77
|
+
relatedProductsIds: ['1', '2'],
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
const configs = [
|
|
81
|
+
{ name: 'featuredProduct', type: 'relation' },
|
|
82
|
+
{ name: 'relatedProducts', type: 'relation' },
|
|
83
|
+
];
|
|
84
|
+
const result = removeReadonlyAndLocalizedCustomFields(values, configs);
|
|
85
|
+
expect(result.customFields).toEqual({
|
|
86
|
+
featuredProductId: '123',
|
|
87
|
+
relatedProductsIds: ['1', '2'],
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('should remove non-permitted relation fields (Id/Ids suffixed)', () => {
|
|
92
|
+
const values = {
|
|
93
|
+
customFields: {
|
|
94
|
+
allowedProductId: '123',
|
|
95
|
+
restrictedProductId: '456',
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
// Only 'allowedProduct' is permitted
|
|
99
|
+
const configs = [{ name: 'allowedProduct', type: 'relation' }];
|
|
100
|
+
const result = removeReadonlyAndLocalizedCustomFields(values, configs);
|
|
101
|
+
expect(result.customFields).toEqual({ allowedProductId: '123' });
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('should remove readonly relation fields with Id suffix', () => {
|
|
105
|
+
const values = {
|
|
106
|
+
customFields: {
|
|
107
|
+
editableProductId: '123',
|
|
108
|
+
lockedProductId: '456',
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
const configs = [
|
|
112
|
+
{ name: 'editableProduct', type: 'relation' },
|
|
113
|
+
{ name: 'lockedProduct', type: 'relation', readonly: true },
|
|
114
|
+
];
|
|
115
|
+
const result = removeReadonlyAndLocalizedCustomFields(values, configs);
|
|
116
|
+
expect(result.customFields).toEqual({ editableProductId: '123' });
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('should remove readonly relation fields with Ids suffix (list relations)', () => {
|
|
120
|
+
const values = {
|
|
121
|
+
customFields: {
|
|
122
|
+
editableItemsIds: ['1', '2'],
|
|
123
|
+
lockedItemsIds: ['3', '4'],
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
const configs = [
|
|
127
|
+
{ name: 'editableItems', type: 'relation' },
|
|
128
|
+
{ name: 'lockedItems', type: 'relation', readonly: true },
|
|
129
|
+
];
|
|
130
|
+
const result = removeReadonlyAndLocalizedCustomFields(values, configs);
|
|
131
|
+
expect(result.customFields).toEqual({ editableItemsIds: ['1', '2'] });
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('should not mutate the original values', () => {
|
|
135
|
+
const values = {
|
|
136
|
+
customFields: {
|
|
137
|
+
allowed: 'yes',
|
|
138
|
+
restricted: 'secret',
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
const configs = [{ name: 'allowed', type: 'string' }];
|
|
142
|
+
removeReadonlyAndLocalizedCustomFields(values, configs);
|
|
143
|
+
expect(values.customFields).toEqual({ allowed: 'yes', restricted: 'secret' });
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
describe('translations', () => {
|
|
147
|
+
it('should remove non-permitted custom fields from translations', () => {
|
|
148
|
+
const values = {
|
|
149
|
+
translations: [
|
|
150
|
+
{
|
|
151
|
+
languageCode: 'en',
|
|
152
|
+
customFields: {
|
|
153
|
+
allowedField: 'hello',
|
|
154
|
+
restrictedField: 'secret',
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
],
|
|
158
|
+
};
|
|
159
|
+
const configs = [{ name: 'allowedField', type: 'localeString' }];
|
|
160
|
+
const result = removeReadonlyAndLocalizedCustomFields(values, configs);
|
|
161
|
+
expect(result.translations[0].customFields).toEqual({ allowedField: 'hello' });
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('should remove readonly custom fields from translations', () => {
|
|
165
|
+
const values = {
|
|
166
|
+
translations: [
|
|
167
|
+
{
|
|
168
|
+
languageCode: 'en',
|
|
169
|
+
customFields: {
|
|
170
|
+
editable: 'can edit',
|
|
171
|
+
locked: 'cannot edit',
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
],
|
|
175
|
+
};
|
|
176
|
+
const configs = [
|
|
177
|
+
{ name: 'editable', type: 'localeString' },
|
|
178
|
+
{ name: 'locked', type: 'localeString', readonly: true },
|
|
179
|
+
];
|
|
180
|
+
const result = removeReadonlyAndLocalizedCustomFields(values, configs);
|
|
181
|
+
expect(result.translations[0].customFields).toEqual({ editable: 'can edit' });
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('should handle multiple translations', () => {
|
|
185
|
+
const values = {
|
|
186
|
+
translations: [
|
|
187
|
+
{
|
|
188
|
+
languageCode: 'en',
|
|
189
|
+
customFields: { allowed: 'hello', restricted: 'secret' },
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
languageCode: 'de',
|
|
193
|
+
customFields: { allowed: 'hallo', restricted: 'geheim' },
|
|
194
|
+
},
|
|
195
|
+
],
|
|
196
|
+
};
|
|
197
|
+
const configs = [{ name: 'allowed', type: 'localeString' }];
|
|
198
|
+
const result = removeReadonlyAndLocalizedCustomFields(values, configs);
|
|
199
|
+
expect(result.translations[0].customFields).toEqual({ allowed: 'hello' });
|
|
200
|
+
expect(result.translations[1].customFields).toEqual({ allowed: 'hallo' });
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('should handle translations without customFields', () => {
|
|
204
|
+
const values = {
|
|
205
|
+
translations: [{ languageCode: 'en', name: 'Test' }],
|
|
206
|
+
};
|
|
207
|
+
const configs = [{ name: 'someField', type: 'localeString' }];
|
|
208
|
+
const result = removeReadonlyAndLocalizedCustomFields(values, configs);
|
|
209
|
+
expect(result.translations[0]).toEqual({ languageCode: 'en', name: 'Test' });
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
describe('combined scenarios', () => {
|
|
214
|
+
it('should handle a mix of readonly, locale, permitted, and non-permitted fields', () => {
|
|
215
|
+
const values = {
|
|
216
|
+
customFields: {
|
|
217
|
+
editableString: 'yes',
|
|
218
|
+
readonlyString: 'no',
|
|
219
|
+
localeName: 'root locale',
|
|
220
|
+
nonPermittedField: 'should go',
|
|
221
|
+
allowedRelationId: '1',
|
|
222
|
+
readonlyRelationId: '2',
|
|
223
|
+
nonPermittedRelationId: '3',
|
|
224
|
+
},
|
|
225
|
+
translations: [
|
|
226
|
+
{
|
|
227
|
+
languageCode: 'en',
|
|
228
|
+
customFields: {
|
|
229
|
+
localeName: 'English name',
|
|
230
|
+
nonPermittedLocale: 'should go',
|
|
231
|
+
},
|
|
232
|
+
},
|
|
233
|
+
],
|
|
234
|
+
};
|
|
235
|
+
const configs = [
|
|
236
|
+
{ name: 'editableString', type: 'string' },
|
|
237
|
+
{ name: 'readonlyString', type: 'string', readonly: true },
|
|
238
|
+
{ name: 'localeName', type: 'localeString' },
|
|
239
|
+
{ name: 'allowedRelation', type: 'relation' },
|
|
240
|
+
{ name: 'readonlyRelation', type: 'relation', readonly: true },
|
|
241
|
+
// 'nonPermittedField' and 'nonPermittedRelation' are not in config
|
|
242
|
+
];
|
|
243
|
+
const result = removeReadonlyAndLocalizedCustomFields(values, configs);
|
|
244
|
+
expect(result.customFields).toEqual({
|
|
245
|
+
editableString: 'yes',
|
|
246
|
+
allowedRelationId: '1',
|
|
247
|
+
});
|
|
248
|
+
expect(result.translations[0].customFields).toEqual({
|
|
249
|
+
localeName: 'English name',
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
});
|
package/src/lib/lib/utils.ts
CHANGED
|
@@ -63,46 +63,77 @@ export function normalizeString(input: string, spaceReplacer = ' '): string {
|
|
|
63
63
|
* Removes any readonly custom fields from form values before submission.
|
|
64
64
|
* Also removes localeString and localeText fields from the root customFields object
|
|
65
65
|
* since they should only exist in the translations array.
|
|
66
|
-
*
|
|
66
|
+
* Additionally removes any custom fields that are not present in the provided config,
|
|
67
|
+
* which handles fields the user does not have permission to update.
|
|
68
|
+
* This prevents errors when submitting readonly or permission-restricted custom field values to mutations.
|
|
67
69
|
*
|
|
68
70
|
* @param values - The form values that may contain custom fields
|
|
69
71
|
* @param customFieldConfigs - Array of custom field configurations for the entity
|
|
70
|
-
*
|
|
72
|
+
* (should already be filtered to only include fields the current user has access to)
|
|
73
|
+
* @returns The values with readonly, locale, and non-permitted custom fields removed
|
|
71
74
|
*/
|
|
72
75
|
export function removeReadonlyAndLocalizedCustomFields<T extends Record<string, any>>(
|
|
73
76
|
values: T,
|
|
74
77
|
customFieldConfigs: Array<{ name: string; readonly?: boolean | null; type?: string }> = [],
|
|
75
78
|
): T {
|
|
76
|
-
if (!values
|
|
79
|
+
if (!values) {
|
|
77
80
|
return values;
|
|
78
81
|
}
|
|
79
82
|
|
|
80
83
|
const result = structuredClone(values);
|
|
81
|
-
const
|
|
82
|
-
.
|
|
83
|
-
|
|
84
|
+
const permittedFieldNames = new Set(
|
|
85
|
+
customFieldConfigs.flatMap(config => {
|
|
86
|
+
if (config.type === 'relation') {
|
|
87
|
+
// Relation fields are transformed to `nameId` or `nameIds` by
|
|
88
|
+
// transformRelationFields() before this function runs
|
|
89
|
+
return [config.name, `${config.name}Id`, `${config.name}Ids`];
|
|
90
|
+
}
|
|
91
|
+
return [config.name];
|
|
92
|
+
}),
|
|
93
|
+
);
|
|
94
|
+
const readonlyFieldNames = customFieldConfigs.flatMap(config => {
|
|
95
|
+
if (config.readonly !== true) return [];
|
|
96
|
+
if (config.type === 'relation') {
|
|
97
|
+
return [config.name, `${config.name}Id`, `${config.name}Ids`];
|
|
98
|
+
}
|
|
99
|
+
return [config.name];
|
|
100
|
+
});
|
|
84
101
|
const localeFieldNames = customFieldConfigs
|
|
85
102
|
.filter(config => config.type === 'localeString' || config.type === 'localeText')
|
|
86
103
|
.map(config => config.name);
|
|
87
104
|
const fieldsToRemoveFromRoot = [...readonlyFieldNames, ...localeFieldNames];
|
|
88
105
|
|
|
89
106
|
if (result.customFields && typeof result.customFields === 'object') {
|
|
107
|
+
for (const fieldName of Object.keys(result.customFields)) {
|
|
108
|
+
if (!permittedFieldNames.has(fieldName)) {
|
|
109
|
+
delete result.customFields[fieldName];
|
|
110
|
+
}
|
|
111
|
+
}
|
|
90
112
|
fieldsToRemoveFromRoot.forEach(fieldName => {
|
|
91
113
|
delete result.customFields[fieldName];
|
|
92
114
|
});
|
|
93
115
|
}
|
|
94
116
|
|
|
95
|
-
|
|
117
|
+
removeNonPermittedAndReadonlyFromTranslations(result, readonlyFieldNames, permittedFieldNames);
|
|
96
118
|
return result;
|
|
97
119
|
}
|
|
98
120
|
|
|
99
|
-
function
|
|
121
|
+
function removeNonPermittedAndReadonlyFromTranslations(
|
|
122
|
+
entity: Record<string, any>,
|
|
123
|
+
readonlyFieldNames: string[],
|
|
124
|
+
permittedFieldNames: Set<string>,
|
|
125
|
+
): void {
|
|
100
126
|
if (!Array.isArray(entity.translations)) {
|
|
101
127
|
return;
|
|
102
128
|
}
|
|
103
129
|
|
|
104
|
-
entity.translations.forEach(translation => {
|
|
130
|
+
entity.translations.forEach((translation: Record<string, any>) => {
|
|
105
131
|
if (translation?.customFields && typeof translation.customFields === 'object') {
|
|
132
|
+
for (const fieldName of Object.keys(translation.customFields)) {
|
|
133
|
+
if (!permittedFieldNames.has(fieldName)) {
|
|
134
|
+
delete translation.customFields[fieldName];
|
|
135
|
+
}
|
|
136
|
+
}
|
|
106
137
|
readonlyFieldNames.forEach(fieldName => {
|
|
107
138
|
delete translation.customFields[fieldName];
|
|
108
139
|
});
|
|
@@ -135,6 +135,9 @@ export function AuthProvider({ children }: Readonly<{ children: React.ReactNode
|
|
|
135
135
|
}
|
|
136
136
|
}, [settings.activeChannelId, currentUserData?.me?.channels]);
|
|
137
137
|
|
|
138
|
+
// Determine isAuthenticated from currentUserData
|
|
139
|
+
const isAuthenticated = !!currentUserData?.me?.id;
|
|
140
|
+
|
|
138
141
|
// Auth actions
|
|
139
142
|
const login = React.useCallback(
|
|
140
143
|
(username: string, password: string, onLoginSuccess?: () => void) => {
|
|
@@ -187,26 +190,62 @@ export function AuthProvider({ children }: Readonly<{ children: React.ReactNode
|
|
|
187
190
|
|
|
188
191
|
const logout = React.useCallback(
|
|
189
192
|
async (onLogoutSuccess?: () => void) => {
|
|
193
|
+
// Clear any stale error from a previous logout attempt. Matches the
|
|
194
|
+
// login() pattern (line 149 below clears it on the success path)
|
|
195
|
+
// and ensures UI doesn't keep rendering a previous failure's
|
|
196
|
+
// message during a retry.
|
|
197
|
+
setAuthenticationError(undefined);
|
|
190
198
|
setIsLoginLogoutInProgress(true);
|
|
191
199
|
setStatus('verifying');
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
200
|
+
// Try block scoped to the mutation call only. Exceptions thrown
|
|
201
|
+
// by the success-branch side-effects below (queryClient.clear,
|
|
202
|
+
// localStorage.removeItem, onLogoutSuccess) propagate naturally
|
|
203
|
+
// rather than being misclassified as transport failures.
|
|
204
|
+
let data;
|
|
205
|
+
try {
|
|
206
|
+
data = await api.mutate(LogOutMutation)({});
|
|
207
|
+
} catch (error) {
|
|
208
|
+
// Network/server failure. Transport failure doesn't tell us
|
|
209
|
+
// whether the server applied the logout, so refetch the
|
|
210
|
+
// current user to determine actual state rather than trusting
|
|
211
|
+
// the cached isAuthenticated snapshot (staleTime: Infinity
|
|
212
|
+
// means react-query won't auto-refetch this query).
|
|
213
|
+
setAuthenticationError(error instanceof Error ? error.message : String(error));
|
|
214
|
+
const { data: refreshedData, error: refreshedError } = await refetchCurrentUser();
|
|
215
|
+
if (refreshedError || !refreshedData?.me?.id) {
|
|
198
216
|
setStatus('unauthenticated');
|
|
199
|
-
|
|
200
|
-
|
|
217
|
+
} else {
|
|
218
|
+
setStatus('authenticated');
|
|
201
219
|
}
|
|
202
|
-
|
|
220
|
+
setIsLoginLogoutInProgress(false);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (data?.logout.success) {
|
|
225
|
+
// Clear all cached queries to prevent stale data
|
|
226
|
+
queryClient.clear();
|
|
227
|
+
try {
|
|
228
|
+
// localStorage can throw (quota exceeded, Safari private
|
|
229
|
+
// mode, security errors, storage disabled). The server-side
|
|
230
|
+
// logout already succeeded — don't let storage cleanup
|
|
231
|
+
// failure block the UI state transition below.
|
|
232
|
+
localStorage.removeItem(LS_KEY_SELECTED_CHANNEL_TOKEN);
|
|
233
|
+
} catch {
|
|
234
|
+
// intentionally swallowed — see comment above
|
|
235
|
+
}
|
|
236
|
+
setStatus('unauthenticated');
|
|
237
|
+
setIsLoginLogoutInProgress(false);
|
|
238
|
+
onLogoutSuccess?.();
|
|
239
|
+
} else {
|
|
240
|
+
// Server responded but reported success=false. Restore the
|
|
241
|
+
// pre-logout authenticated status so the UI is interactive again.
|
|
242
|
+
setStatus(isAuthenticated ? 'authenticated' : 'unauthenticated');
|
|
243
|
+
setIsLoginLogoutInProgress(false);
|
|
244
|
+
}
|
|
203
245
|
},
|
|
204
|
-
[queryClient],
|
|
246
|
+
[queryClient, isAuthenticated, refetchCurrentUser],
|
|
205
247
|
);
|
|
206
248
|
|
|
207
|
-
// Determine isAuthenticated from currentUserData
|
|
208
|
-
const isAuthenticated = !!currentUserData?.me?.id;
|
|
209
|
-
|
|
210
249
|
// Handle status transitions based on query state
|
|
211
250
|
React.useEffect(() => {
|
|
212
251
|
// Don't change status if we're in the middle of login/logout
|