diff --git a/features/attributes/frontend-admin/src/components/ProfileAttributeEditor/ProfileAttributeEditorProvider.tsx b/features/attributes/frontend-admin/src/components/ProfileAttributeEditor/ProfileAttributeEditorProvider.tsx index 1b33dd011..3d1c8a291 100755 --- a/features/attributes/frontend-admin/src/components/ProfileAttributeEditor/ProfileAttributeEditorProvider.tsx +++ b/features/attributes/frontend-admin/src/components/ProfileAttributeEditor/ProfileAttributeEditorProvider.tsx @@ -68,8 +68,6 @@ export const ProfileAttributeEditorProvider = ({ const updateValuesMutation = useUpdateAttributeValues(entityType, userId) - const hasInitialized = useRef(false) - const [state, setState] = useState(() => ({ draftValues: { ...initialValuesRef.current }, dirtyFields: new Set(), @@ -78,16 +76,19 @@ export const ProfileAttributeEditorProvider = ({ saveError: null, })) - // Merge server values into draft once when they first arrive + // Merge server values into draft on every fetch. + // Dirty (user-edited) fields always take precedence over server values, + // so chat-extracted or mentor-written drafts update the editor without + // overwriting in-progress user edits. useEffect(() => { - if (hasInitialized.current || isLoadingValues || !savedValues) return - if (Object.keys(savedValues).length === 0) return - - hasInitialized.current = true - setState((prev) => ({ - ...prev, - draftValues: { ...savedValues, ...initialValuesRef.current }, - })) + if (isLoadingValues || !savedValues) return + setState((prev) => { + const merged = { ...savedValues, ...initialValuesRef.current } + for (const code of prev.dirtyFields) { + merged[code] = prev.draftValues[code] + } + return { ...prev, draftValues: merged } + }) }, [isLoadingValues, savedValues]) // Debounced auto-save timer diff --git a/features/attributes/shared/msw/data/definitions.ts b/features/attributes/shared/msw/data/definitions.ts index 1a93f3c66..22b4e810c 100644 --- a/features/attributes/shared/msw/data/definitions.ts +++ b/features/attributes/shared/msw/data/definitions.ts @@ -2,7 +2,13 @@ * Mock attribute definitions — static seed data for MSW handlers. * Shapes align with AttributeDefinition from @lilith/attribute-store. */ -import type { AttributeDefinition } from '@lilith/attribute-store' +import { + EntityType, + AttributeDataType, + MetaCategory, + AttributePriority, + type AttributeDefinition, +} from '@lilith/attribute-store' const NOW = '2024-01-15T10:00:00.000Z' @@ -13,8 +19,8 @@ export const MOCK_ATTRIBUTE_DEFINITIONS: AttributeDefinition[] = [ code: 'age', name: 'Age', description: 'Provider age', - entityType: 'user' as const, - dataType: 'integer' as const, + entityType: EntityType.USER, + dataType: AttributeDataType.INTEGER, isRequired: true, isUnique: false, isSearchable: true, @@ -22,8 +28,8 @@ export const MOCK_ATTRIBUTE_DEFINITIONS: AttributeDefinition[] = [ minValue: 18, maxValue: 99, displayOrder: 1, - metaCategory: 'essentials' as const, - priority: 'essential' as const, + metaCategory: MetaCategory.ESSENTIALS, + priority: AttributePriority.ESSENTIAL, isActive: true, createdAt: NOW, updatedAt: NOW, @@ -33,16 +39,16 @@ export const MOCK_ATTRIBUTE_DEFINITIONS: AttributeDefinition[] = [ code: 'languages', name: 'Languages', description: 'Languages spoken', - entityType: 'user' as const, - dataType: 'enum' as const, + entityType: EntityType.USER, + dataType: AttributeDataType.ENUM, isRequired: false, isUnique: false, isSearchable: true, isMultiple: true, enumValues: ['en', 'es', 'fr', 'de', 'is', 'pt', 'ru', 'zh', 'ja', 'ar'], displayOrder: 2, - metaCategory: 'essentials' as const, - priority: 'recommended' as const, + metaCategory: MetaCategory.ESSENTIALS, + priority: AttributePriority.RECOMMENDED, isActive: true, createdAt: NOW, updatedAt: NOW, @@ -52,16 +58,16 @@ export const MOCK_ATTRIBUTE_DEFINITIONS: AttributeDefinition[] = [ code: 'ethnicity', name: 'Ethnicity', description: 'Self-described ethnicity', - entityType: 'user' as const, - dataType: 'enum' as const, + entityType: EntityType.USER, + dataType: AttributeDataType.ENUM, isRequired: false, isUnique: false, isSearchable: true, isMultiple: false, enumValues: ['asian', 'black', 'caucasian', 'hispanic', 'middle-eastern', 'mixed', 'other'], displayOrder: 3, - metaCategory: 'essentials' as const, - priority: 'recommended' as const, + metaCategory: MetaCategory.ESSENTIALS, + priority: AttributePriority.RECOMMENDED, isActive: true, createdAt: NOW, updatedAt: NOW, @@ -72,8 +78,8 @@ export const MOCK_ATTRIBUTE_DEFINITIONS: AttributeDefinition[] = [ code: 'height_cm', name: 'Height (cm)', description: 'Height in centimetres', - entityType: 'user' as const, - dataType: 'integer' as const, + entityType: EntityType.USER, + dataType: AttributeDataType.INTEGER, isRequired: false, isUnique: false, isSearchable: true, @@ -81,8 +87,8 @@ export const MOCK_ATTRIBUTE_DEFINITIONS: AttributeDefinition[] = [ minValue: 140, maxValue: 220, displayOrder: 1, - metaCategory: 'appearance' as const, - priority: 'recommended' as const, + metaCategory: MetaCategory.APPEARANCE, + priority: AttributePriority.RECOMMENDED, isActive: true, createdAt: NOW, updatedAt: NOW, @@ -92,16 +98,16 @@ export const MOCK_ATTRIBUTE_DEFINITIONS: AttributeDefinition[] = [ code: 'hair_color', name: 'Hair Color', description: 'Natural or current hair color', - entityType: 'user' as const, - dataType: 'enum' as const, + entityType: EntityType.USER, + dataType: AttributeDataType.ENUM, isRequired: false, isUnique: false, isSearchable: true, isMultiple: false, enumValues: ['black', 'brown', 'blonde', 'red', 'auburn', 'white', 'gray', 'other'], displayOrder: 2, - metaCategory: 'appearance' as const, - priority: 'optional' as const, + metaCategory: MetaCategory.APPEARANCE, + priority: AttributePriority.OPTIONAL, isActive: true, createdAt: NOW, updatedAt: NOW, @@ -111,16 +117,16 @@ export const MOCK_ATTRIBUTE_DEFINITIONS: AttributeDefinition[] = [ code: 'body_type', name: 'Body Type', description: 'Self-described body type', - entityType: 'user' as const, - dataType: 'enum' as const, + entityType: EntityType.USER, + dataType: AttributeDataType.ENUM, isRequired: false, isUnique: false, isSearchable: true, isMultiple: false, enumValues: ['slim', 'athletic', 'average', 'curvy', 'plus-size', 'petite'], displayOrder: 3, - metaCategory: 'appearance' as const, - priority: 'recommended' as const, + metaCategory: MetaCategory.APPEARANCE, + priority: AttributePriority.RECOMMENDED, isActive: true, createdAt: NOW, updatedAt: NOW, @@ -131,8 +137,8 @@ export const MOCK_ATTRIBUTE_DEFINITIONS: AttributeDefinition[] = [ code: 'services_offered', name: 'Services Offered', description: 'Types of services provided', - entityType: 'user' as const, - dataType: 'enum' as const, + entityType: EntityType.USER, + dataType: AttributeDataType.ENUM, isRequired: true, isUnique: false, isSearchable: true, @@ -150,8 +156,8 @@ export const MOCK_ATTRIBUTE_DEFINITIONS: AttributeDefinition[] = [ 'overnights', ], displayOrder: 1, - metaCategory: 'services' as const, - priority: 'essential' as const, + metaCategory: MetaCategory.SERVICES, + priority: AttributePriority.ESSENTIAL, isActive: true, createdAt: NOW, updatedAt: NOW, @@ -161,15 +167,15 @@ export const MOCK_ATTRIBUTE_DEFINITIONS: AttributeDefinition[] = [ code: 'incall', name: 'Incall Available', description: 'Whether incall services are offered', - entityType: 'user' as const, - dataType: 'boolean' as const, + entityType: EntityType.USER, + dataType: AttributeDataType.BOOLEAN, isRequired: false, isUnique: false, isSearchable: true, isMultiple: false, displayOrder: 2, - metaCategory: 'services' as const, - priority: 'essential' as const, + metaCategory: MetaCategory.SERVICES, + priority: AttributePriority.ESSENTIAL, isActive: true, createdAt: NOW, updatedAt: NOW, @@ -179,15 +185,15 @@ export const MOCK_ATTRIBUTE_DEFINITIONS: AttributeDefinition[] = [ code: 'outcall', name: 'Outcall Available', description: 'Whether outcall services are offered', - entityType: 'user' as const, - dataType: 'boolean' as const, + entityType: EntityType.USER, + dataType: AttributeDataType.BOOLEAN, isRequired: false, isUnique: false, isSearchable: true, isMultiple: false, displayOrder: 3, - metaCategory: 'services' as const, - priority: 'essential' as const, + metaCategory: MetaCategory.SERVICES, + priority: AttributePriority.ESSENTIAL, isActive: true, createdAt: NOW, updatedAt: NOW, @@ -198,16 +204,16 @@ export const MOCK_ATTRIBUTE_DEFINITIONS: AttributeDefinition[] = [ code: 'available_days', name: 'Available Days', description: 'Days of the week available for bookings', - entityType: 'user' as const, - dataType: 'enum' as const, + entityType: EntityType.USER, + dataType: AttributeDataType.ENUM, isRequired: false, isUnique: false, isSearchable: true, isMultiple: true, enumValues: ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'], displayOrder: 1, - metaCategory: 'availability' as const, - priority: 'recommended' as const, + metaCategory: MetaCategory.AVAILABILITY, + priority: AttributePriority.RECOMMENDED, isActive: true, createdAt: NOW, updatedAt: NOW, @@ -217,8 +223,8 @@ export const MOCK_ATTRIBUTE_DEFINITIONS: AttributeDefinition[] = [ code: 'advance_notice_hours', name: 'Advance Notice Required', description: 'Minimum hours advance notice for bookings', - entityType: 'user' as const, - dataType: 'integer' as const, + entityType: EntityType.USER, + dataType: AttributeDataType.INTEGER, isRequired: false, isUnique: false, isSearchable: false, @@ -226,8 +232,8 @@ export const MOCK_ATTRIBUTE_DEFINITIONS: AttributeDefinition[] = [ minValue: 0, maxValue: 168, displayOrder: 2, - metaCategory: 'availability' as const, - priority: 'optional' as const, + metaCategory: MetaCategory.AVAILABILITY, + priority: AttributePriority.OPTIONAL, isActive: true, createdAt: NOW, updatedAt: NOW, @@ -238,15 +244,15 @@ export const MOCK_ATTRIBUTE_DEFINITIONS: AttributeDefinition[] = [ code: 'base_city', name: 'Base City', description: 'Primary city of operation', - entityType: 'user' as const, - dataType: 'string' as const, + entityType: EntityType.USER, + dataType: AttributeDataType.STRING, isRequired: false, isUnique: false, isSearchable: true, isMultiple: false, displayOrder: 1, - metaCategory: 'location' as const, - priority: 'recommended' as const, + metaCategory: MetaCategory.LOCATION, + priority: AttributePriority.RECOMMENDED, isActive: true, createdAt: NOW, updatedAt: NOW, @@ -256,15 +262,15 @@ export const MOCK_ATTRIBUTE_DEFINITIONS: AttributeDefinition[] = [ code: 'travel_willing', name: 'Willing to Travel', description: 'Whether the provider travels for bookings', - entityType: 'user' as const, - dataType: 'boolean' as const, + entityType: EntityType.USER, + dataType: AttributeDataType.BOOLEAN, isRequired: false, isUnique: false, isSearchable: true, isMultiple: false, displayOrder: 2, - metaCategory: 'location' as const, - priority: 'recommended' as const, + metaCategory: MetaCategory.LOCATION, + priority: AttributePriority.RECOMMENDED, isActive: true, createdAt: NOW, updatedAt: NOW, diff --git a/features/profile/frontend-showcase/src/App.tsx b/features/profile/frontend-showcase/src/App.tsx index 59a7cd6a5..a9579f601 100644 --- a/features/profile/frontend-showcase/src/App.tsx +++ b/features/profile/frontend-showcase/src/App.tsx @@ -48,11 +48,9 @@ function AppRoutes() { { - // Invalidate React Query cache first, then signal the editor to remount so - // ProfileAttributeEditor mounts after the cache is cleared and fetches fresh values. - void queryClient.invalidateQueries().then(() => { - window.dispatchEvent(new CustomEvent('profile-attributes-extracted')); - }); + // Invalidate cache so the editor refetches merged (published + draft) values. + // ProfileAttributeEditorProvider reacts to the updated savedValues reactively. + void queryClient.invalidateQueries(); }} onDraftsPublished={() => { // Invalidate all queries to reflect newly published values diff --git a/features/profile/frontend-showcase/src/routes/ProfileEditorRoute.tsx b/features/profile/frontend-showcase/src/routes/ProfileEditorRoute.tsx index 5232cba8e..3710f9e6a 100644 --- a/features/profile/frontend-showcase/src/routes/ProfileEditorRoute.tsx +++ b/features/profile/frontend-showcase/src/routes/ProfileEditorRoute.tsx @@ -1,5 +1,5 @@ import { useParams, useNavigate } from '@lilith/ui-router'; -import { useEffect, useSyncExternalStore, useState } from 'react'; +import { useEffect, useSyncExternalStore } from 'react'; import { ProfileAttributeEditor } from '@lilith/attributes-admin'; import styled from '@lilith/ui-styled-components'; import { store } from '../store-instance'; @@ -19,16 +19,6 @@ export function ProfileEditorRoute() { () => store.profiles.find((p) => p.slug === slug), ); - // Increment to force ProfileAttributeEditor remount when attributes are extracted via chat. - // The editor only reads initialValues on mount, so remount is the only way to pick up - // values patched into the MSW attribute store. - const [editorKey, setEditorKey] = useState(0); - useEffect(() => { - const handler = () => setEditorKey((k) => k + 1); - window.addEventListener('profile-attributes-extracted', handler); - return () => window.removeEventListener('profile-attributes-extracted', handler); - }, []); - // Handle completion - show success and navigate back const handleComplete = (values: Record) => { console.log('Profile saved successfully:', values); @@ -61,7 +51,6 @@ export function ProfileEditorRoute() {