* refactor: apply biome formatting to apps/web (batch 1) Formats apps/web non-modules directories: - apps/web/app - apps/web/lib - apps/web/pages - apps/web/styles - apps/web/server - apps/web/test - Root-level .ts and .mjs files Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * refactor: apply biome formatting to apps/web/modules (batch 2) Formats smaller apps/web/modules directories: - onboarding, data-table, users, apps, auth - integration-attribute-sync, shell, availability - feature-flags, team, webhooks, getting-started - feature-opt-in, troubleshooter, schedules, notifications - signup-view.tsx Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * refactor: apply biome formatting to apps/web/modules (batch 3) Formats medium apps/web/modules directories: - bookings - event-types - insights - embed Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * refactor: apply biome formatting to apps/web/modules/ee (batch 4) Formats apps/web/modules/ee directory. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * refactor: apply biome formatting to apps/web (batch 5) Formats remaining apps/web directories: - apps/web/modules/settings - apps/web/modules/booking-audit - apps/web/playwright Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * refactor: apply biome formatting to apps/web/components (batch 6) Formats remaining apps/web/components files. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * refactor: apply biome formatting to new apps/web files from main Formats newly added/modified files from main merge: - WorkflowStepContainer.tsx (resolved merge conflicts) - Newly moved files (blocklist, form-builder, schedules, etc.) - Other files modified in main Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * refactor: apply biome formatting to new apps/web files from main Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
195 lines
6.0 KiB
TypeScript
195 lines
6.0 KiB
TypeScript
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
|
import { Button } from "@calcom/ui/components/button";
|
|
import { Input, Select, Switch } from "@calcom/ui/components/form";
|
|
import { Icon } from "@calcom/ui/components/icon";
|
|
|
|
import { getDefaultFieldMapping } from "@calcom/features/ee/integration-attribute-sync/lib/fieldMappingHelpers";
|
|
import type {
|
|
IFieldMappingFormState,
|
|
IFieldMappingWithOptionalId,
|
|
} from "@calcom/features/ee/integration-attribute-sync/repositories/IIntegrationAttributeSyncRepository";
|
|
|
|
type AttributeOptions = {
|
|
label: string;
|
|
value: string;
|
|
}[];
|
|
|
|
interface FieldMappingBuilderProps {
|
|
value: IFieldMappingFormState;
|
|
onChange: (value: IFieldMappingFormState) => void;
|
|
attributeOptions: AttributeOptions;
|
|
isLoadingAttributes?: boolean;
|
|
}
|
|
|
|
interface MappingRowProps {
|
|
mapping: IFieldMappingWithOptionalId;
|
|
onChange: (mapping: IFieldMappingWithOptionalId) => void;
|
|
onRemove: () => void;
|
|
attributeOptions: AttributeOptions;
|
|
isLoading?: boolean;
|
|
error?: string;
|
|
}
|
|
|
|
const MappingRow = ({ mapping, onChange, onRemove, attributeOptions, isLoading, error }: MappingRowProps) => {
|
|
const { t } = useLocale();
|
|
|
|
const selectedAttribute = attributeOptions.find((opt) => opt.value === mapping.attributeId);
|
|
|
|
const handleFieldNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
onChange({
|
|
...mapping,
|
|
integrationFieldName: e.target.value,
|
|
});
|
|
};
|
|
|
|
const handleAttributeChange = (selected: { value: string; label: string } | null) => {
|
|
if (!selected) return;
|
|
onChange({
|
|
...mapping,
|
|
attributeId: selected.value,
|
|
});
|
|
};
|
|
|
|
const handleEnabledChange = (checked: boolean) => {
|
|
onChange({
|
|
...mapping,
|
|
enabled: checked,
|
|
});
|
|
};
|
|
|
|
return (
|
|
<div>
|
|
<div
|
|
className={`bg-default border-subtle flex flex-wrap items-center gap-3 rounded-lg border p-3 ${error ? "border-red-500" : ""}`}>
|
|
<div className="flex-1" style={{ minWidth: "200px" }}>
|
|
<Input
|
|
type="text"
|
|
value={mapping.integrationFieldName}
|
|
onChange={handleFieldNameChange}
|
|
disabled={isLoading}
|
|
placeholder={t("attribute_sync_field_placeholder")}
|
|
className="h-7 text-sm"
|
|
/>
|
|
</div>
|
|
|
|
<Icon name="arrow-right" className="text-subtle h-4 w-4" />
|
|
|
|
<div className="flex-1" style={{ minWidth: "200px" }}>
|
|
<Select
|
|
size="sm"
|
|
placeholder={t("attribute_sync_select_attribute")}
|
|
options={attributeOptions}
|
|
value={selectedAttribute}
|
|
onChange={handleAttributeChange}
|
|
isLoading={isLoading}
|
|
isDisabled={isLoading}
|
|
className="w-full"
|
|
/>
|
|
</div>
|
|
|
|
<Switch checked={mapping.enabled} onCheckedChange={handleEnabledChange} disabled={isLoading} />
|
|
|
|
<Button
|
|
color="minimal"
|
|
size="sm"
|
|
StartIcon="trash-2"
|
|
onClick={onRemove}
|
|
disabled={isLoading}
|
|
className="text-subtle hover:text-default"
|
|
aria-label={t("remove")}
|
|
/>
|
|
</div>
|
|
{error && <p className="mt-1 text-xs text-red-600">{error}</p>}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export const FieldMappingBuilder = ({
|
|
value,
|
|
onChange,
|
|
attributeOptions,
|
|
isLoadingAttributes,
|
|
}: FieldMappingBuilderProps) => {
|
|
const { t } = useLocale();
|
|
|
|
// Find indices of rows with duplicate attribute mappings
|
|
const duplicateIndices = new Set<number>();
|
|
const seenAttributes = new Map<string, number>(); // attributeId -> first index
|
|
value.mappings.forEach((mapping: IFieldMappingWithOptionalId, index: number) => {
|
|
if (mapping.attributeId) {
|
|
const firstIndex = seenAttributes.get(mapping.attributeId);
|
|
if (firstIndex !== undefined) {
|
|
// Mark both the first occurrence and current as duplicates
|
|
duplicateIndices.add(firstIndex);
|
|
duplicateIndices.add(index);
|
|
} else {
|
|
seenAttributes.set(mapping.attributeId, index);
|
|
}
|
|
}
|
|
});
|
|
|
|
const handleAddMapping = () => {
|
|
onChange({
|
|
...value,
|
|
mappings: [...value.mappings, getDefaultFieldMapping()],
|
|
});
|
|
};
|
|
|
|
const handleMappingChange = (index: number, newMapping: IFieldMappingWithOptionalId) => {
|
|
const newMappings = [...value.mappings];
|
|
newMappings[index] = newMapping;
|
|
onChange({
|
|
...value,
|
|
mappings: newMappings,
|
|
});
|
|
};
|
|
|
|
const handleRemoveMapping = (index: number) => {
|
|
onChange({
|
|
...value,
|
|
mappings: value.mappings.filter((_: IFieldMappingWithOptionalId, i: number) => i !== index),
|
|
});
|
|
};
|
|
|
|
return (
|
|
<div className="bg-default border-subtle rounded-2xl border p-2">
|
|
<div className="ml-2 flex items-center gap-0.5">
|
|
<div className="border-subtle rounded-lg border p-1">
|
|
<Icon name="link" className="text-subtle h-4 w-4" />
|
|
</div>
|
|
<span className="text-emphasis ml-2 text-sm font-medium">{t("attribute_sync_field_mappings")}</span>
|
|
</div>
|
|
|
|
<div className="bg-muted mt-2 space-y-2 rounded-xl p-2">
|
|
{value.mappings.length === 0 ? (
|
|
<div className="text-subtle py-6 text-center text-sm">{t("attribute_sync_no_mappings")}</div>
|
|
) : (
|
|
value.mappings.map((mapping: IFieldMappingWithOptionalId, index: number) => (
|
|
<MappingRow
|
|
key={mapping.id || `new-${index}`}
|
|
mapping={mapping}
|
|
onChange={(newMapping) => handleMappingChange(index, newMapping)}
|
|
onRemove={() => handleRemoveMapping(index)}
|
|
attributeOptions={attributeOptions}
|
|
isLoading={isLoadingAttributes}
|
|
error={
|
|
duplicateIndices.has(index) ? t("attribute_sync_duplicate_attribute_mapping") : undefined
|
|
}
|
|
/>
|
|
))
|
|
)}
|
|
</div>
|
|
|
|
<Button
|
|
color="minimal"
|
|
size="sm"
|
|
StartIcon="plus"
|
|
className="mt-2"
|
|
onClick={handleAddMapping}
|
|
disabled={isLoadingAttributes}>
|
|
{t("attribute_sync_add_mapping")}
|
|
</Button>
|
|
</div>
|
|
);
|
|
};
|