@@ -21,6 +21,7 @@ import {
2121} from ' ../sharedImports' ;
2222import { Tabs } from ' ../../../../../../../../../shared/components/molecules/tabs' ;
2323import { CollaborationTab } from ' ../../../../../../../../../shared/components/organisms/collaboration-tab' ;
24+ import { duplicateSalesChannelSelectValueMappingMutation } from ' ../../../../../../../../../shared/api/mutations/salesChannels.js' ;
2425import type { FormConfig , QueryFormField , ChoiceFormField } from ' ../sharedImports' ;
2526import type {
2627 MapPropertyDataResult ,
@@ -39,9 +40,11 @@ const { t } = useI18n();
3940const valueId = ref (String (route .params .id ));
4041const type = ref (String (route .params .type ));
4142const integrationId = route .query .integrationId ? route .query .integrationId .toString () : ' ' ;
42- const salesChannelId = route .query .salesChannelId ? route .query .salesChannelId .toString () : ' ' ;
43+ const routeSalesChannelId = route .query .salesChannelId ? route .query .salesChannelId .toString () : ' ' ;
4344const isWizard = route .query .wizard === ' 1' ;
4445
46+ const valueProxyId = ref <string | null >(null );
47+ const resolvedSalesChannelId = ref <string | null >(routeSalesChannelId || null );
4548const propertyId = ref <string | null >(null );
4649const propertyName = ref (' ' );
4750const propertyType = ref <string | null >(null );
@@ -56,8 +59,9 @@ const propertyMapped = ref(true);
5659const placeholderContext: RemoteSelectValueEditPropertyContext = {
5760 type: type .value ,
5861 valueId: valueId .value ,
62+ valueProxyId: null ,
5963 integrationId ,
60- salesChannelId ,
64+ salesChannelId: routeSalesChannelId ,
6165 isWizard ,
6266 propertyId: null ,
6367 propertyName: ' ' ,
@@ -84,8 +88,9 @@ placeholderContext.form = form;
8488const createContext = (): RemoteSelectValueEditPropertyContext => ({
8589 type: type .value ,
8690 valueId: valueId .value ,
91+ valueProxyId: valueProxyId .value ,
8792 integrationId ,
88- salesChannelId ,
93+ salesChannelId: resolvedSalesChannelId . value || routeSalesChannelId ,
8994 isWizard ,
9095 propertyId: propertyId .value ,
9196 propertyName: propertyName .value ,
@@ -117,6 +122,10 @@ const localInstanceField = ref<QueryFormField | null>(null);
117122const recommendations = ref <Recommendation []>([]);
118123const loadingRecommendations = ref (false );
119124const formErrors = ref <Record <string , any >>({});
125+ const showDuplicateMapping = ref (false );
126+ const duplicateLocalInstanceId = ref <string | null >(null );
127+ const duplicateErrors = ref <Record <string , any >>({});
128+ const duplicatingMapping = ref (false );
120129
121130const remoteFields = computed (() => props .config .remoteFields );
122131const contextState = computed (() => createContext ());
@@ -161,6 +170,31 @@ const generateValuePath = computed(() => {
161170 return props .config .generateValuePath ? props .config .generateValuePath (contextState .value ) : null ;
162171});
163172const notMappedBannerLink = computed (() => (props .config .notMappedBanner ? props .config .notMappedBanner .linkPath (contextState .value ) : null ));
173+ const duplicateSectionVisible = computed (() => {
174+ if (
175+ ! props .config .duplicateMapping ||
176+ ! localInstanceField .value ||
177+ ! valueProxyId .value ||
178+ ! resolvedSalesChannelId .value ||
179+ ! form [props .config .localInstanceFieldKey ]?.id
180+ ) {
181+ return false ;
182+ }
183+ if (props .config .duplicateMapping .isVisible ) {
184+ return props .config .duplicateMapping .isVisible (contextState .value );
185+ }
186+ return ! isBooleanMapping .value ;
187+ });
188+ const duplicateLocalInstanceField = computed <QueryFormField | null >(() => {
189+ if (! localInstanceField .value ) {
190+ return null ;
191+ }
192+
193+ return {
194+ ... localInstanceField .value ,
195+ name: ' duplicateLocalInstance' ,
196+ } as QueryFormField ;
197+ });
164198
165199const breadcrumbsLinks = computed (() => [
166200 { path: { name: ' integrations.integrations.list' }, name: t (' integrations.title' ) },
@@ -195,6 +229,12 @@ const applyValueData = (result: MapValueDataResult | null | undefined) => {
195229 if (! result ) {
196230 return ;
197231 }
232+ if (result .valueProxyId !== undefined ) {
233+ valueProxyId .value = result .valueProxyId ;
234+ }
235+ if (result .salesChannelId !== undefined && result .salesChannelId ) {
236+ resolvedSalesChannelId .value = result .salesChannelId ;
237+ }
198238 if (result .form ) {
199239 Object .entries (result .form ).forEach (([key , value ]) => {
200240 if (key === props .config .localInstanceFieldKey && typeof value === ' object' && value !== null ) {
@@ -316,12 +356,21 @@ watch(localPropertyId, () => {
316356
317357watch (
318358 () => form [props .config .localInstanceFieldKey ]?.id ,
319- () => {
320- const currentId = form [props .config .localInstanceFieldKey ]?.id ;
359+ (currentId ) => {
321360 recommendations .value = recommendations .value .filter (recommendation => recommendation .id !== currentId );
361+ if (duplicateLocalInstanceId .value && duplicateLocalInstanceId .value === currentId ) {
362+ duplicateLocalInstanceId .value = null ;
363+ }
364+ delete duplicateErrors .value .localInstance ;
322365 }
323366);
324367
368+ watch (duplicateSectionVisible , (visible ) => {
369+ if (! visible ) {
370+ resetDuplicateSection ();
371+ }
372+ });
373+
325374const selectRecommendation = (id : string ) => {
326375 if (! form [props .config .localInstanceFieldKey ]) {
327376 form [props .config .localInstanceFieldKey ] = { id };
@@ -331,6 +380,64 @@ const selectRecommendation = (id: string) => {
331380 recommendations .value = recommendations .value .filter (recommendation => recommendation .id !== id );
332381};
333382
383+ const resetDuplicateSection = () => {
384+ showDuplicateMapping .value = false ;
385+ duplicateLocalInstanceId .value = null ;
386+ duplicateErrors .value = {};
387+ };
388+
389+ const submitDuplicateMapping = async () => {
390+ if (! props .config .duplicateMapping || ! valueProxyId .value || ! resolvedSalesChannelId .value ) {
391+ return ;
392+ }
393+
394+ duplicateErrors .value = {};
395+
396+ if (! duplicateLocalInstanceId .value ) {
397+ duplicateErrors .value .localInstance = t (' integrations.show.propertySelectValues.duplicate.validation.required' );
398+ return ;
399+ }
400+
401+ if (duplicateLocalInstanceId .value === form [props .config .localInstanceFieldKey ]?.id ) {
402+ duplicateErrors .value .localInstance = t (' integrations.show.propertySelectValues.duplicate.validation.sameValue' );
403+ return ;
404+ }
405+
406+ try {
407+ duplicatingMapping .value = true ;
408+ const { data } = await apolloClient .mutate ({
409+ mutation: duplicateSalesChannelSelectValueMappingMutation ,
410+ variables: {
411+ salesChannel: { id: resolvedSalesChannelId .value },
412+ remotePropertySelectValue: { id: valueProxyId .value },
413+ localInstance: { id: duplicateLocalInstanceId .value },
414+ },
415+ });
416+
417+ const newMappingId = data ?.duplicateSalesChannelSelectValueMapping ?.id ;
418+ if (! newMappingId ) {
419+ throw new Error (t (' integrations.show.propertySelectValues.duplicate.error' ));
420+ }
421+
422+ if (props .config .duplicateMapping .successMessageKey ) {
423+ Toast .success (t (props .config .duplicateMapping .successMessageKey ));
424+ }
425+
426+ await router .push ({
427+ name: ' integrations.remotePropertySelectValues.edit' ,
428+ params: { type: type .value , id: newMappingId },
429+ query: { integrationId , salesChannelId: resolvedSalesChannelId .value },
430+ });
431+ } catch (error : any ) {
432+ const graphQLError = error ?.graphQLErrors ?.[0 ]?.message ;
433+ const message = graphQLError || error ?.message || t (' integrations.show.propertySelectValues.duplicate.error' );
434+ duplicateErrors .value .__all__ = message ;
435+ Toast .error (String (message ));
436+ } finally {
437+ duplicatingMapping .value = false ;
438+ }
439+ };
440+
334441const handleSubmitErrors = (errors : Record <string , any >) => {
335442 formErrors .value = errors || {};
336443 const messages = Object .values (formErrors .value ).filter (Boolean );
@@ -429,7 +536,7 @@ onMounted(async () => {
429536 enhancedConfig .value .submitUrl = {
430537 name: ' integrations.remotePropertySelectValues.edit' ,
431538 params: { type: type .value , id: nextId },
432- query: { integrationId , salesChannelId , wizard: ' 1' },
539+ query: { integrationId , salesChannelId: resolvedSalesChannelId . value || routeSalesChannelId , wizard: ' 1' },
433540 };
434541 enhancedConfig .value .submitLabel = t (props .config .wizardSubmitLabelKey ?? ' integrations.show.mapping.saveAndMapNext' );
435542 return ;
@@ -572,6 +679,66 @@ onMounted(async () => {
572679 <p v-else class =" text-sm text-gray-500" >{{ t('integrations.show.propertySelectValues.recommendation.none') }}</p >
573680 </div >
574681 </div >
682+ <div
683+ v-if =" duplicateSectionVisible && duplicateLocalInstanceField && props.config.duplicateMapping"
684+ class =" mt-5 rounded border border-gray-200 bg-gray-50 p-5"
685+ >
686+ <div class =" flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between" >
687+ <div class =" space-y-2" >
688+ <div class =" inline-flex items-center rounded-full bg-gray-100 px-2.5 py-1 text-xs font-semibold uppercase tracking-[0.12em] text-gray-500" >
689+ {{ t('integrations.show.propertySelectValues.duplicate.eyebrow') }}
690+ </div >
691+ <div >
692+ <h3 class =" text-base font-semibold text-gray-900" >{{ t(props.config.duplicateMapping.titleKey) }}</h3 >
693+ <p class =" mt-1 max-w-2xl text-sm leading-6 text-gray-500" >{{ t(props.config.duplicateMapping.descriptionKey) }}</p >
694+ </div >
695+ </div >
696+ <div class =" flex w-full flex-col gap-2 sm:w-auto sm:flex-row" >
697+ <button
698+ type =" button"
699+ class =" inline-flex items-center justify-center rounded-xl border px-4 py-2 text-sm font-medium transition"
700+ :class =" showDuplicateMapping ? 'border-gray-200 bg-white text-gray-600 hover:border-gray-300 hover:bg-gray-50' : 'border-emerald-200 bg-emerald-50 text-emerald-700 hover:border-emerald-300 hover:bg-emerald-100'"
701+ @click =" showDuplicateMapping = true"
702+ >
703+ {{ t('shared.labels.yes') }}
704+ </button >
705+ <button
706+ type =" button"
707+ class =" inline-flex items-center justify-center rounded-xl border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-600 transition hover:border-gray-300 hover:bg-gray-50"
708+ @click =" resetDuplicateSection"
709+ >
710+ {{ t('shared.labels.no') }}
711+ </button >
712+ </div >
713+ </div >
714+
715+ <div v-if =" showDuplicateMapping" class =" mt-4 space-y-3 border-t border-gray-200 pt-4" >
716+ <div >
717+ <Label class =" font-semibold block text-sm leading-6 text-gray-900" >{{ t('integrations.show.propertySelectValues.duplicate.selectorLabel') }}</Label >
718+ <p class =" mt-1 text-sm leading-6 text-gray-500" >{{ t(props.config.duplicateMapping.promptKey) }}</p >
719+ </div >
720+ <FieldQuery
721+ :field =" duplicateLocalInstanceField as QueryFormField"
722+ v-model =" duplicateLocalInstanceId"
723+ @update:modelValue =" duplicateLocalInstanceId = $event"
724+ />
725+ <p v-if =" props.config.duplicateMapping.selectorHelpKey" class =" mt-2 text-sm leading-6 text-gray-400" >
726+ {{ t(props.config.duplicateMapping.selectorHelpKey) }}
727+ </p >
728+ <p v-if =" duplicateErrors.localInstance" class =" mt-2 text-sm leading-6 text-red-600" >{{ duplicateErrors.localInstance }}</p >
729+ <p v-if =" duplicateErrors.__all__" class =" mt-2 text-sm leading-6 text-red-600" >{{ duplicateErrors.__all__ }}</p >
730+ <div class =" flex justify-end" >
731+ <button
732+ type =" button"
733+ class =" inline-flex items-center justify-center rounded-xl bg-gray-900 px-4 py-2.5 text-sm font-semibold text-white transition hover:bg-gray-800 disabled:cursor-not-allowed disabled:bg-gray-300"
734+ :disabled =" duplicatingMapping || !duplicateLocalInstanceId"
735+ @click =" submitDuplicateMapping"
736+ >
737+ {{ duplicatingMapping ? t('integrations.show.propertySelectValues.duplicate.submitting') : t(props.config.duplicateMapping.submitLabelKey) }}
738+ </button >
739+ </div >
740+ </div >
741+ </div >
575742 </div >
576743 <div class =" col-span-full" >
577744 <slot name =" additional-fields" :form =" form" :context =" contextState" />
0 commit comments