sms-extension-1777538290/client/src/pages/GlobalSms.jsx

1702 lines
69 KiB
JavaScript

import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import apiClient from '../api/client';
import { useBusiness } from '../context/BusinessContext';
const BASE_PROFILE_KEYS = new Set(['providerName', 'senderId', 'dltEntityId']);
const PENDING_SENDER_ID_PROFILE_NAME = 'Pending Sender ID';
const WORKSPACE_TABS = [
{ id: 'authorization', label: 'Authorization' },
{ id: 'headers', label: 'Headers' },
{ id: 'body', label: 'Body' },
{ id: 'curl', label: 'Raw cURL' },
];
const CURL_DATA_FLAGS = new Set(['--data', '--data-raw', '--data-binary', '-d']);
function formatAuthMode(value) {
const normalized = String(value || '').trim().toLowerCase();
if (!normalized) return 'Not inferred';
if (normalized === 'api_key') return 'API Key';
if (normalized === 'bearer') return 'Bearer Token';
if (normalized === 'basic') return 'Basic Auth';
return normalized.charAt(0).toUpperCase() + normalized.slice(1);
}
function buildProfilePatchPayload(inputs = [], values = {}) {
const provider = {};
const profileInputValues = {};
inputs.forEach((input) => {
const rawValue = String(values[input.key] ?? '').trim();
if (!rawValue) return;
if (BASE_PROFILE_KEYS.has(input.key)) {
provider[input.key] = input.key === 'senderId' ? rawValue.toUpperCase() : rawValue;
return;
}
profileInputValues[input.key] = rawValue;
});
return {
...(Object.keys(provider).length > 0 ? { provider } : {}),
...(Object.keys(profileInputValues).length > 0 ? { profileInputValues } : {}),
};
}
function getInputInitialValues(inputs = []) {
return inputs.reduce((accumulator, input) => {
accumulator[input.key] = input.value || '';
return accumulator;
}, {});
}
function mergeProfileInputs(...groups) {
const mergedInputs = new Map();
groups.forEach((group) => {
(Array.isArray(group) ? group : []).forEach((input) => {
const key = String(input?.key || '').trim();
if (!key || input?.source === 'runtime') return;
const current = mergedInputs.get(key) || {};
mergedInputs.set(key, {
...current,
...input,
key,
label: input?.label || current.label || key,
required: input?.required !== false || current.required === true,
secret: input?.secret === true || current.secret === true,
hasValue: input?.hasValue === true || current.hasValue === true,
maskedValue: input?.maskedValue || current.maskedValue || '',
value: Object.prototype.hasOwnProperty.call(input || {}, 'value')
? input.value
: (current.value || ''),
});
});
});
return Array.from(mergedInputs.values());
}
function isRelevantPersistentProfileInput(input, missingInputKeys = new Set()) {
if (!input || input.source === 'runtime') return false;
if (input.secret !== true) return false;
const token = String(input.token || '').trim();
if (token) return true;
return missingInputKeys.has(String(input.key || '').trim());
}
function skipShellIndentation(input, index) {
let cursor = index;
while (cursor < input.length && /[\t \f\v\u00a0]/.test(input[cursor])) {
cursor += 1;
}
return cursor;
}
function normalizeCurlCommand(command) {
const input = String(command || '')
.replace(/\r\n/g, '\n')
.replace(/\r/g, '\n');
let output = '';
let quote = null;
for (let index = 0; index < input.length; index += 1) {
const char = input[index];
if (quote === '\'') {
output += char;
if (char === '\'') quote = null;
continue;
}
if (quote === '"') {
output += char;
if (char === '\\' && index + 1 < input.length) {
output += input[index + 1];
index += 1;
continue;
}
if (char === '"') {
quote = null;
}
continue;
}
if (char === '\'' || char === '"') {
quote = char;
output += char;
continue;
}
if (char === '\\') {
const nextChar = input[index + 1];
if (nextChar === '\n') {
output += ' ';
index = skipShellIndentation(input, index + 2) - 1;
continue;
}
if (nextChar === 'n') {
output += ' ';
index = skipShellIndentation(input, index + 2) - 1;
continue;
}
if (nextChar === 'r' && input[index + 2] === 'n') {
output += ' ';
index = skipShellIndentation(input, index + 3) - 1;
continue;
}
}
output += char;
}
return output.trim();
}
function tokenizeCurlCommand(command) {
const input = normalizeCurlCommand(command);
const tokens = [];
let current = '';
let quote = null;
let escaping = false;
for (let index = 0; index < input.length; index += 1) {
const char = input[index];
if (escaping) {
current += char;
escaping = false;
continue;
}
if (quote === '\'') {
if (char === '\'') {
quote = null;
} else {
current += char;
}
continue;
}
if (quote === '"') {
if (char === '"') {
quote = null;
continue;
}
if (char === '\\') {
const nextChar = input[index + 1];
if (nextChar) {
current += nextChar;
index += 1;
continue;
}
}
current += char;
continue;
}
if (char === '\\') {
escaping = true;
continue;
}
if (char === '\'' || char === '"') {
quote = char;
continue;
}
if (/\s/.test(char)) {
if (current) {
tokens.push(current);
current = '';
}
continue;
}
current += char;
}
if (current) {
tokens.push(current);
}
return tokens;
}
function getCurlDataArguments(args = []) {
const dataArgs = [];
for (let index = 0; index < args.length; index += 1) {
const argument = args[index];
if (CURL_DATA_FLAGS.has(argument) && index + 1 < args.length) {
dataArgs.push(String(args[index + 1] || ''));
index += 1;
continue;
}
const flag = Array.from(CURL_DATA_FLAGS).find((entry) => argument.startsWith(`${entry}=`));
if (flag) {
dataArgs.push(argument.slice(flag.length + 1));
}
}
return dataArgs;
}
function parseStructuredBodyValue(value = '') {
const trimmed = String(value || '').trim();
if (!trimmed) return null;
try {
return JSON.parse(trimmed);
} catch {
if (!(trimmed.startsWith('"') && trimmed.endsWith('"'))) {
return null;
}
}
try {
const parsedString = JSON.parse(trimmed);
if (typeof parsedString !== 'string') {
return parsedString;
}
return JSON.parse(parsedString);
} catch {
return null;
}
}
function isLikelyFormEncoded(value = '') {
const normalized = String(value || '').trim();
return normalized.includes('=') && !normalized.startsWith('{') && !normalized.startsWith('[');
}
function buildBodyPreviewFromCurl(curlStr = '') {
const source = String(curlStr || '').trim();
if (!source) {
return { type: 'empty', label: 'Body', content: '' };
}
if (!source.toLowerCase().startsWith('curl')) {
return { type: 'empty', label: 'Body', content: '' };
}
try {
const tokens = tokenizeCurlCommand(source);
if (tokens.length === 0 || tokens[0] !== 'curl') {
return { type: 'empty', label: 'Body', content: '' };
}
const dataArgs = getCurlDataArguments(tokens.slice(1));
if (dataArgs.length === 0) {
return { type: 'none', label: 'No Body', content: 'No request body detected.' };
}
if (dataArgs.length === 1) {
const structuredValue = parseStructuredBodyValue(dataArgs[0]);
if (structuredValue !== null) {
return {
type: 'json',
label: 'JSON Body',
content: JSON.stringify(structuredValue, null, 2),
};
}
}
const allFormEncoded = dataArgs.every((entry) => isLikelyFormEncoded(entry) && parseStructuredBodyValue(entry) === null);
if (allFormEncoded) {
const formLines = [];
dataArgs.forEach((entry) => {
const params = new URLSearchParams(String(entry || ''));
Array.from(params.entries()).forEach(([key, value]) => {
formLines.push(`${key}=${value}`);
});
});
return {
type: 'form',
label: 'Form Body',
content: formLines.join('\n'),
};
}
return {
type: 'text',
label: dataArgs.length === 1 ? 'Raw Body' : 'Combined Body',
content: dataArgs.join('\n\n'),
};
} catch {
return {
type: 'invalid',
label: 'Body',
content: 'Unable to parse the request body from this cURL.',
};
}
}
function cloneRequestPreview(requestPreview = null) {
if (!requestPreview || typeof requestPreview !== 'object') {
return {
method: 'POST',
url: '',
maskedUrl: '',
urlMasked: false,
headers: [],
};
}
return {
method: requestPreview.method || 'POST',
url: requestPreview.url || '',
maskedUrl: requestPreview.maskedUrl || requestPreview.url || '',
urlMasked: requestPreview.urlMasked === true,
headers: Array.isArray(requestPreview.headers)
? requestPreview.headers.map((header, index) => ({
id: header?.id || `header-${index}`,
key: header?.key || '',
value: header?.value || '',
maskedValue: header?.maskedValue || header?.value || '',
masked: header?.masked === true,
secret: header?.secret === true,
enabled: header?.enabled !== false,
}))
: [],
};
}
function areRequestPreviewsEqual(left = null, right = null) {
const leftRequest = cloneRequestPreview(left);
const rightRequest = cloneRequestPreview(right);
if (leftRequest.url !== rightRequest.url) return false;
if (leftRequest.headers.length !== rightRequest.headers.length) return false;
return leftRequest.headers.every((header, index) => {
const comparison = rightRequest.headers[index];
if (!comparison) return false;
return header.id === comparison.id
&& header.key === comparison.key
&& header.value === comparison.value
&& header.enabled === comparison.enabled;
});
}
function hasInputFormChanges(initialValues = {}, currentValues = {}) {
const keys = new Set([
...Object.keys(initialValues || {}),
...Object.keys(currentValues || {}),
]);
for (const key of keys) {
if (String(initialValues?.[key] ?? '') !== String(currentValues?.[key] ?? '')) {
return true;
}
}
return false;
}
function mergeRevealIntoRequestPreview(targetRequest, revealRequest, options = {}) {
const baseRequest = cloneRequestPreview(targetRequest);
const revealedRequest = cloneRequestPreview(revealRequest);
const force = options.force === true;
return {
...baseRequest,
url: force || baseRequest.urlMasked ? (revealedRequest.url || baseRequest.url) : baseRequest.url,
maskedUrl: revealedRequest.maskedUrl || baseRequest.maskedUrl,
urlMasked: force ? false : baseRequest.urlMasked,
headers: baseRequest.headers.map((header, index) => {
const revealedHeader = revealedRequest.headers.find((candidate) => candidate.id === header.id) || revealedRequest.headers[index];
if (!revealedHeader) return header;
const remainsMaskable = header.masked === true || revealedHeader.masked === true;
if (force || remainsMaskable) {
return {
...header,
value: revealedHeader.value,
maskedValue: revealedHeader.maskedValue || header.maskedValue,
masked: remainsMaskable,
secret: revealedHeader.secret ?? header.secret,
};
}
return {
...header,
maskedValue: revealedHeader.maskedValue || header.maskedValue,
secret: revealedHeader.secret ?? header.secret,
};
}),
};
}
function DeleteProfileModal({ preview, loading, deleting, onCancel, onConfirm }) {
if (!preview) return null;
const impactedTemplates = Array.isArray(preview.impactedTemplates) ? preview.impactedTemplates : [];
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-gray-900/55 p-4 backdrop-blur-sm">
<div className="w-full max-w-xl rounded-2xl border border-gray-200 bg-white shadow-xl">
<div className="border-b border-gray-200 px-6 py-5">
<h3 className="text-lg font-bold text-gray-900">Delete cURL Profile</h3>
<p className="mt-1 text-sm text-gray-500">
{preview.profile?.name || 'This profile'} will be deleted. Bound templates will be removed, but event definitions will stay.
</p>
</div>
<div className="space-y-4 px-6 py-5">
{loading ? (
<div className="flex items-center gap-3 rounded-xl border border-gray-200 bg-gray-50 px-4 py-4 text-sm text-gray-600">
<span className="h-5 w-5 animate-spin rounded-full border-2 border-gray-200 border-t-primary-blue" />
Loading bound templates
</div>
) : impactedTemplates.length > 0 ? (
<>
<p className="text-sm font-semibold text-gray-900">Affected templates</p>
<div className="max-h-72 space-y-3 overflow-y-auto">
{impactedTemplates.map((template) => (
<div key={`${template.eventSlug}-${template.status}`} className="rounded-xl border border-gray-200 bg-gray-50 px-4 py-3">
<div className="flex items-center justify-between gap-3">
<p className="font-medium text-gray-900">{template.eventLabel || template.eventSlug}</p>
<span className="rounded-full border border-gray-200 bg-white px-2.5 py-1 text-[11px] font-semibold uppercase tracking-[0.14em] text-gray-500">
{template.status || 'generated'}
</span>
</div>
{template.templateId && (
<p className="mt-2 font-mono text-xs text-gray-500">DLT Template ID: {template.templateId}</p>
)}
</div>
))}
</div>
</>
) : (
<div className="rounded-xl border border-gray-200 bg-gray-50 px-4 py-3 text-sm text-gray-600">
No templates are currently bound to this profile.
</div>
)}
</div>
<div className="flex gap-3 border-t border-gray-200 px-6 py-4">
<button
type="button"
onClick={onCancel}
disabled={loading || deleting}
className="flex-1 rounded-lg border border-gray-300 px-4 py-2 text-sm font-semibold text-gray-700 transition hover:bg-gray-50 disabled:opacity-50"
>
Cancel
</button>
<button
type="button"
onClick={onConfirm}
disabled={loading || deleting}
className="flex-1 rounded-lg bg-red-600 px-4 py-2 text-sm font-semibold text-white transition hover:bg-red-700 disabled:opacity-50"
>
{deleting ? 'Deleting…' : 'Delete Profile'}
</button>
</div>
</div>
</div>
);
}
export default function GlobalSms() {
const { businessId } = useParams();
const navigate = useNavigate();
const { isSetupComplete, setHasGlobalSms, setIsSetupComplete } = useBusiness();
const hasLoadedProfilesRef = useRef(false);
const requestPreviewVersionRef = useRef({});
const urlEditorRef = useRef(null);
const [initialLoading, setInitialLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
const [profiles, setProfiles] = useState([]);
const [activeProfileId, setActiveProfileId] = useState(null);
const [selectedProfileId, setSelectedProfileId] = useState(null);
const [workspaceTab, setWorkspaceTab] = useState('authorization');
const [attemptedSave, setAttemptedSave] = useState(false);
const [showImportModal, setShowImportModal] = useState(false);
const [saving, setSaving] = useState(false);
const [savingInputs, setSavingInputs] = useState(false);
const [error, setError] = useState('');
const [success, setSuccess] = useState('');
const [formCurl, setFormCurl] = useState('');
const [formSetActive, setFormSetActive] = useState(true);
const [inputForm, setInputForm] = useState({});
const [requestDrafts, setRequestDrafts] = useState({});
const [savedRequestPreviews, setSavedRequestPreviews] = useState({});
const [editingUrlProfileId, setEditingUrlProfileId] = useState('');
const [editingHeader, setEditingHeader] = useState(null);
const [revealedProfiles, setRevealedProfiles] = useState({});
const [curlVisibleProfileIds, setCurlVisibleProfileIds] = useState({});
const [headerVisibleProfileIds, setHeaderVisibleProfileIds] = useState({});
const [inputVisibility, setInputVisibility] = useState({});
const [deletePreview, setDeletePreview] = useState(null);
const [loadingDeleteImpactId, setLoadingDeleteImpactId] = useState('');
const [deletingProfileId, setDeletingProfileId] = useState('');
const selectedProfile = useMemo(
() => profiles.find((profile) => profile.id === selectedProfileId)
|| profiles.find((profile) => profile.id === activeProfileId)
|| profiles[0]
|| null,
[profiles, selectedProfileId, activeProfileId],
);
const profileInputs = useMemo(
() => (selectedProfile?.profileInputs || []).filter((input) => input.source !== 'runtime'),
[selectedProfile],
);
const missingInputs = useMemo(
() => selectedProfile?.executionReadiness?.missingProfileInputs || [],
[selectedProfile],
);
const missingInputKeys = useMemo(
() => new Set(missingInputs.map((input) => input.key)),
[missingInputs],
);
const authorizationInputs = useMemo(
() => mergeProfileInputs(
profileInputs.filter((input) => isRelevantPersistentProfileInput(input, missingInputKeys)),
missingInputs,
),
[profileInputs, missingInputKeys, missingInputs],
);
const isSelectedProfileActive = selectedProfile?.id === activeProfileId;
const initialInputValues = useMemo(() => getInputInitialValues(authorizationInputs), [authorizationInputs]);
const selectedSavedRequest = useMemo(
() => (selectedProfile ? cloneRequestPreview(savedRequestPreviews[selectedProfile.id] || selectedProfile.requestPreview) : null),
[savedRequestPreviews, selectedProfile],
);
const selectedRequestDraft = useMemo(
() => (selectedProfile ? cloneRequestPreview(requestDrafts[selectedProfile.id] || selectedProfile.requestPreview) : null),
[requestDrafts, selectedProfile],
);
const hasRequestChanges = useMemo(
() => (selectedProfile ? !areRequestPreviewsEqual(selectedRequestDraft, selectedSavedRequest) : false),
[selectedProfile, selectedRequestDraft, selectedSavedRequest],
);
const hasInputChanges = useMemo(
() => (selectedProfile ? hasInputFormChanges(initialInputValues, inputForm) : false),
[initialInputValues, inputForm, selectedProfile],
);
const hasPendingProfileChanges = selectedProfile ? (hasRequestChanges || hasInputChanges) : false;
const eventsPath = `/${businessId}/events`;
const loadProfiles = useCallback(async ({ background = false } = {}) => {
try {
if (background) {
setRefreshing(true);
} else {
setInitialLoading(true);
}
const response = await apiClient.get(`/api/businesses/${businessId}/global-sms/profiles`);
const fetchedProfiles = response.data?.profiles || [];
const nextActiveProfileId = response.data?.activeProfileId || null;
const nextActiveProfile = fetchedProfiles.find((profile) => profile.id === nextActiveProfileId) || null;
const nextIsSetupComplete = nextActiveProfile?.executionReadiness?.isSetupComplete === true;
setProfiles(fetchedProfiles);
setActiveProfileId(nextActiveProfileId);
setHasGlobalSms(fetchedProfiles.length > 0);
setIsSetupComplete(nextIsSetupComplete);
setError('');
return {
profiles: fetchedProfiles,
activeProfile: nextActiveProfile,
activeProfileId: nextActiveProfileId,
hasProfile: !!nextActiveProfile,
complete: nextIsSetupComplete,
};
} catch {
setError('Failed to load cURL profiles');
setHasGlobalSms(false);
setIsSetupComplete(false);
return {
profiles: [],
activeProfile: null,
activeProfileId: null,
hasProfile: false,
complete: false,
};
} finally {
if (background) {
setRefreshing(false);
} else {
hasLoadedProfilesRef.current = true;
setInitialLoading(false);
}
}
}, [businessId, setHasGlobalSms, setIsSetupComplete]);
useEffect(() => {
loadProfiles({ background: hasLoadedProfilesRef.current });
}, [loadProfiles]);
useEffect(() => {
if (!profiles.length) {
setSelectedProfileId(null);
return;
}
if (selectedProfileId && profiles.some((profile) => profile.id === selectedProfileId)) {
return;
}
setSelectedProfileId(activeProfileId || profiles[0]?.id || null);
}, [profiles, activeProfileId, selectedProfileId]);
useEffect(() => {
setAttemptedSave(false);
setInputForm(initialInputValues);
}, [initialInputValues, selectedProfile?.id]);
useEffect(() => {
if (!success) return undefined;
const timeoutId = window.setTimeout(() => setSuccess(''), 2800);
return () => window.clearTimeout(timeoutId);
}, [success]);
useEffect(() => {
if (!selectedProfile?.id) return;
const nextVersion = selectedProfile.updatedAt || `${selectedProfile.id}:${selectedProfile.requestPreview?.headers?.length || 0}`;
if (requestPreviewVersionRef.current[selectedProfile.id] === nextVersion) return;
const nextRequestPreview = cloneRequestPreview(selectedProfile.requestPreview);
requestPreviewVersionRef.current[selectedProfile.id] = nextVersion;
setSavedRequestPreviews((current) => ({
...current,
[selectedProfile.id]: nextRequestPreview,
}));
setRequestDrafts((current) => ({
...current,
[selectedProfile.id]: cloneRequestPreview(nextRequestPreview),
}));
setRevealedProfiles((current) => {
if (!current[selectedProfile.id]) return current;
const nextState = { ...current };
delete nextState[selectedProfile.id];
return nextState;
});
setCurlVisibleProfileIds((current) => {
if (!current[selectedProfile.id]) return current;
const nextState = { ...current };
delete nextState[selectedProfile.id];
return nextState;
});
setHeaderVisibleProfileIds((current) => {
if (!current[selectedProfile.id]) return current;
const nextState = { ...current };
delete nextState[selectedProfile.id];
return nextState;
});
}, [selectedProfile]);
const clearTransientRevealState = useCallback(() => {
setCurlVisibleProfileIds({});
setHeaderVisibleProfileIds({});
setInputVisibility({});
}, []);
useEffect(() => {
setEditingHeader(null);
setEditingUrlProfileId('');
clearTransientRevealState();
}, [clearTransientRevealState, workspaceTab, selectedProfile?.id]);
useEffect(() => {
const handleHide = () => {
setEditingHeader(null);
setEditingUrlProfileId('');
clearTransientRevealState();
};
const handleVisibilityChange = () => {
if (document.hidden) {
handleHide();
}
};
window.addEventListener('blur', handleHide);
document.addEventListener('visibilitychange', handleVisibilityChange);
return () => {
window.removeEventListener('blur', handleHide);
document.removeEventListener('visibilitychange', handleVisibilityChange);
};
}, [clearTransientRevealState]);
useEffect(() => {
if (!editingUrlProfileId || !urlEditorRef.current) return;
urlEditorRef.current.focus();
urlEditorRef.current.select();
}, [editingUrlProfileId, selectedProfile?.id, selectedRequestDraft?.url, selectedRequestDraft?.maskedUrl, selectedRequestDraft?.urlMasked]);
const ensureRevealData = useCallback(async (profileId) => {
if (revealedProfiles[profileId]) return revealedProfiles[profileId];
const response = await apiClient.get(`/api/businesses/${businessId}/global-sms/profiles/${profileId}/reveal`);
setRevealedProfiles((current) => ({ ...current, [profileId]: response.data }));
return response.data;
}, [businessId, revealedProfiles]);
const applyRevealedRequestPreview = useCallback((profileId, revealRequestPreview) => {
if (!profileId || !revealRequestPreview) return;
setSavedRequestPreviews((current) => ({
...current,
[profileId]: mergeRevealIntoRequestPreview(current[profileId], revealRequestPreview, { force: true }),
}));
setRequestDrafts((current) => ({
...current,
[profileId]: mergeRevealIntoRequestPreview(current[profileId], revealRequestPreview),
}));
}, []);
const updateRequestDraft = useCallback((profileId, updater) => {
if (!profileId) return;
setRequestDrafts((current) => {
const baseDraft = cloneRequestPreview(current[profileId] || selectedProfile?.requestPreview);
const nextDraft = typeof updater === 'function' ? updater(baseDraft) : updater;
return {
...current,
[profileId]: cloneRequestPreview(nextDraft),
};
});
}, [selectedProfile]);
const mergeRequestPayloadWithReveal = useCallback((draftRequest, savedRequest, revealRequest) => {
const draft = cloneRequestPreview(draftRequest);
const saved = cloneRequestPreview(savedRequest);
const revealed = cloneRequestPreview(revealRequest);
return {
url: saved.urlMasked && draft.url === saved.url ? (revealed.url || draft.url) : draft.url,
headers: draft.headers.map((header, index) => {
const savedHeader = saved.headers.find((candidate) => candidate.id === header.id) || saved.headers[index];
const revealedHeader = revealed.headers.find((candidate) => candidate.id === header.id) || revealed.headers[index];
const shouldRestoreRevealedValue = savedHeader?.masked === true && header.value === savedHeader.value;
return {
id: header.id,
key: header.key,
value: shouldRestoreRevealedValue ? (revealedHeader?.value || header.value) : header.value,
enabled: header.enabled !== false,
};
}),
};
}, []);
async function handleSubmit(event) {
event.preventDefault();
if (!formCurl.trim()) return;
setSaving(true);
setError('');
setSuccess('');
const shouldAutoAdvance = !isSetupComplete;
try {
const response = await apiClient.post(`/api/businesses/${businessId}/global-sms/profiles`, {
rawCurl: formCurl.trim(),
setActive: formSetActive,
});
const createdProfileId = response.data?.id || response.data?.profile?.id || null;
setFormCurl('');
setFormSetActive(true);
setSuccess('Profile created successfully.');
setShowImportModal(false);
const nextState = await loadProfiles({ background: true });
const nextSelectedProfileId = createdProfileId || nextState.activeProfileId || nextState.profiles[0]?.id || null;
const createdProfile = nextState.profiles.find((profile) => profile.id === nextSelectedProfileId) || null;
setSelectedProfileId(nextSelectedProfileId);
setWorkspaceTab(createdProfile?.executionReadiness?.missingProfileInputs?.length ? 'authorization' : 'headers');
if (shouldAutoAdvance && nextState.complete) {
navigate(eventsPath);
}
} catch (saveError) {
setError(saveError.response?.data?.error || 'Failed to save cURL profile');
} finally {
setSaving(false);
}
}
async function handleActivate(profileId) {
const shouldAutoAdvance = !isSetupComplete;
setError('');
setSuccess('');
try {
await apiClient.post(`/api/businesses/${businessId}/global-sms/profiles/${profileId}/activate`);
const nextState = await loadProfiles({ background: true });
setSelectedProfileId(profileId);
setSuccess('Active profile updated.');
if (shouldAutoAdvance && nextState.complete) {
navigate(eventsPath);
}
} catch (activateError) {
setError(activateError.response?.data?.error || 'Failed to activate profile');
}
}
async function handleCopyCurl(profile) {
try {
const revealData = await ensureRevealData(profile.id);
const textToCopy = revealData?.rawCurl || profile.maskedCurl || '';
if (!textToCopy) return;
await navigator.clipboard.writeText(textToCopy);
setSuccess(`Copied ${profile.name} cURL.`);
} catch {
setError('Failed to copy the cURL command.');
}
}
async function handleToggleCurl(profile) {
setError('');
if (!curlVisibleProfileIds[profile.id]) {
try {
const revealData = await ensureRevealData(profile.id);
applyRevealedRequestPreview(profile.id, revealData?.requestPreview);
} catch (revealError) {
setError(revealError.response?.data?.error || 'Failed to reveal stored cURL');
return;
}
}
setCurlVisibleProfileIds((current) => ({
...current,
[profile.id]: !current[profile.id],
}));
}
async function handleToggleHeaderReveal(profile, header) {
if (!profile?.id || !header?.id || header.masked !== true) return;
setError('');
const currentVisibility = headerVisibleProfileIds[profile.id]?.[header.id] === true;
if (!currentVisibility) {
try {
const revealData = await ensureRevealData(profile.id);
applyRevealedRequestPreview(profile.id, revealData?.requestPreview);
} catch (revealError) {
setError(revealError.response?.data?.error || 'Failed to reveal stored header value');
return;
}
}
setHeaderVisibleProfileIds((current) => ({
...current,
[profile.id]: {
...(current[profile.id] || {}),
[header.id]: !currentVisibility,
},
}));
}
async function handleToggleInputVisibility(input) {
if (!selectedProfile?.id || !input?.key || input.secret !== true) return;
const isVisible = inputVisibility[input.key] === true;
if (!isVisible && input.hasValue) {
try {
await ensureRevealData(selectedProfile.id);
} catch (revealError) {
setError(revealError.response?.data?.error || 'Failed to reveal stored authorization value');
return;
}
}
setInputVisibility((current) => ({
...current,
[input.key]: !isVisible,
}));
}
async function handleBeginUrlEdit() {
if (!selectedProfile?.id) return;
if (selectedSavedRequest?.urlMasked) {
try {
const revealData = await ensureRevealData(selectedProfile.id);
applyRevealedRequestPreview(selectedProfile.id, revealData?.requestPreview);
} catch (revealError) {
setError(revealError.response?.data?.error || 'Failed to reveal stored request URL');
return;
}
}
setEditingUrlProfileId(selectedProfile.id);
}
async function handleBeginHeaderEdit(headerId, field, header) {
if (!selectedProfile?.id) return;
if (field === 'value' && header?.masked) {
try {
const revealData = await ensureRevealData(selectedProfile.id);
applyRevealedRequestPreview(selectedProfile.id, revealData?.requestPreview);
} catch (revealError) {
setError(revealError.response?.data?.error || 'Failed to reveal stored header value');
return;
}
setHeaderVisibleProfileIds((current) => ({
...current,
[selectedProfile.id]: {
...(current[selectedProfile.id] || {}),
[headerId]: true,
},
}));
}
setEditingHeader({
profileId: selectedProfile.id,
headerId,
field,
});
}
async function handleSaveSelectedProfile() {
if (!selectedProfile?.id) return;
if (!hasPendingProfileChanges) return;
setSavingInputs(true);
setError('');
setSuccess('');
const shouldAutoAdvance = !isSetupComplete && isSelectedProfileActive;
try {
const payload = {};
if (hasRequestChanges && selectedRequestDraft && selectedSavedRequest) {
let requestPayload = {
url: selectedRequestDraft.url,
headers: selectedRequestDraft.headers.map((header) => ({
id: header.id,
key: header.key,
value: header.value,
enabled: header.enabled !== false,
})),
};
const needsRevealMerge = selectedSavedRequest.urlMasked || selectedSavedRequest.headers.some((header) => header.masked);
if (needsRevealMerge) {
const revealData = await ensureRevealData(selectedProfile.id);
applyRevealedRequestPreview(selectedProfile.id, revealData?.requestPreview);
requestPayload = mergeRequestPayloadWithReveal(selectedRequestDraft, selectedSavedRequest, revealData?.requestPreview);
}
payload.request = requestPayload;
}
if (hasInputChanges) {
Object.assign(payload, buildProfilePatchPayload(authorizationInputs, inputForm));
}
if (!Object.keys(payload).length) {
return;
}
await apiClient.patch(`/api/businesses/${businessId}/global-sms/profiles/${selectedProfile.id}`, payload);
const nextState = await loadProfiles({ background: true });
setSelectedProfileId(selectedProfile.id);
setSuccess('Changes saved.');
if (shouldAutoAdvance && nextState.complete) {
navigate(eventsPath);
}
} catch (patchError) {
setError(patchError.response?.data?.error || 'Failed to save profile changes');
} finally {
setSavingInputs(false);
}
}
async function handleDeleteRequest(profile) {
setError('');
setDeletePreview({
profile: {
id: profile.id,
name: profile.name,
},
impactedTemplates: null,
});
setLoadingDeleteImpactId(profile.id);
try {
const response = await apiClient.get(`/api/businesses/${businessId}/global-sms/profiles/${profile.id}/delete-impact`);
setDeletePreview(response.data);
} catch (deleteImpactError) {
setDeletePreview(null);
setError(deleteImpactError.response?.data?.error || 'Failed to load delete impact');
} finally {
setLoadingDeleteImpactId('');
}
}
async function handleDeleteConfirm() {
if (!deletePreview?.profile?.id) return;
const deletedProfileId = deletePreview.profile.id;
setDeletingProfileId(deletedProfileId);
setError('');
setSuccess('');
try {
await apiClient.delete(`/api/businesses/${businessId}/global-sms/profiles/${deletedProfileId}`);
setDeletePreview(null);
setRevealedProfiles((current) => {
const nextState = { ...current };
delete nextState[deletedProfileId];
return nextState;
});
setCurlVisibleProfileIds((current) => {
const nextState = { ...current };
delete nextState[deletedProfileId];
return nextState;
});
setHeaderVisibleProfileIds((current) => {
const nextState = { ...current };
delete nextState[deletedProfileId];
return nextState;
});
setSavedRequestPreviews((current) => {
const nextState = { ...current };
delete nextState[deletedProfileId];
return nextState;
});
setRequestDrafts((current) => {
const nextState = { ...current };
delete nextState[deletedProfileId];
return nextState;
});
const nextState = await loadProfiles({ background: true });
setSelectedProfileId(nextState.activeProfileId || nextState.profiles[0]?.id || null);
setSuccess('Profile deleted successfully.');
} catch (deleteError) {
setError(deleteError.response?.data?.error || 'Failed to delete profile');
} finally {
setDeletingProfileId('');
}
}
const selectedRevealData = selectedProfile ? revealedProfiles[selectedProfile.id] : null;
const isSelectedCurlVisible = selectedProfile ? curlVisibleProfileIds[selectedProfile.id] === true : false;
const selectedDisplayCurl = selectedProfile
? (isSelectedCurlVisible
? (selectedRevealData?.rawCurl || selectedProfile.maskedCurl || selectedProfile.curlAnalysis?.normalizedCurlTemplate || '')
: (selectedProfile.maskedCurl || selectedProfile.curlAnalysis?.normalizedCurlTemplate || ''))
: '';
const selectedBodyPreview = useMemo(
() => buildBodyPreviewFromCurl(selectedProfile ? selectedDisplayCurl : formCurl),
[formCurl, selectedDisplayCurl, selectedProfile],
);
const selectedRevealProfileInputs = useMemo(
() => Object.fromEntries(
(selectedRevealData?.profileInputs || []).map((input) => [input.key, input]),
),
[selectedRevealData],
);
const selectedHeaderVisibility = selectedProfile ? (headerVisibleProfileIds[selectedProfile.id] || {}) : {};
const selectedRequestUrl = selectedRequestDraft
? ((editingUrlProfileId === selectedProfile?.id || !selectedRequestDraft.urlMasked)
? selectedRequestDraft.url
: selectedRequestDraft.maskedUrl)
: '';
const handleValidateAndSave = async (event) => {
event.preventDefault();
if (saving || savingInputs) return;
if (selectedProfile) {
const hasEmpty = missingInputs.some((input) => !String(inputForm[input.key] || '').trim());
if (hasEmpty) {
setAttemptedSave(true);
setWorkspaceTab('authorization');
return;
}
await handleSaveSelectedProfile();
return;
}
if (!selectedProfile && formCurl) {
await handleSubmit(new Event('submit'));
}
};
const beautifyCurl = (curlStr) => {
if (!curlStr) return 'No cURL stored.';
let out = curlStr
.replace(/ (-X|--request) /g, ' \\\n $1 ')
.replace(/ (--url) /g, ' \\\n $1 ')
.replace(/ (-H|--header) /g, ' \\\n $1 ')
.replace(/ (-d|--data-raw|--data-binary|--data) /g, ' \\\n $1 ');
out = out.replace(/(-d|--data-raw|--data-binary|--data)\s+'([^]*?)'/m, (match, flag, body) => {
try { return `${flag} '\n${JSON.stringify(JSON.parse(body), null, 2)}\n'`; }
catch { return match; }
});
out = out.replace(/(-d|--data-raw|--data-binary|--data)\s+"([^]*?)"/m, (match, flag, body) => {
try { return `${flag} "\n${JSON.stringify(JSON.parse(body.replace(/\\"/g, '"')), null, 2)}\n"`; }
catch { return match; }
});
return out;
};
if (initialLoading) {
return (
<div className="flex h-64 items-center justify-center">
<span className="h-8 w-8 animate-spin rounded-full border-4 border-spinner-track border-t-primary-blue" />
</div>
);
}
return (
<>
<DeleteProfileModal
preview={deletePreview}
loading={loadingDeleteImpactId === deletePreview?.profile?.id}
deleting={deletingProfileId === deletePreview?.profile?.id}
onCancel={() => setDeletePreview(null)}
onConfirm={handleDeleteConfirm}
/>
{showImportModal && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-gray-900/60 p-4 backdrop-blur-sm shadow-2xl">
<div className="w-full max-w-2xl overflow-hidden rounded-3xl border border-gray-200 bg-white shadow-xl">
<div className="border-b border-gray-100 bg-gray-50/50 px-6 py-5">
<p className="mb-1 text-[11px] font-bold uppercase tracking-[0.2em] text-text-muted">Request Builder</p>
<h3 className="text-xl font-bold tracking-tight text-gray-900">Import Provider cURL</h3>
</div>
<div className="px-6 py-6">
<form onSubmit={handleSubmit} className="space-y-6">
<div className="grid gap-5 md:grid-cols-[minmax(0,1fr)_auto]">
<label className="self-end rounded-xl border border-gray-200 bg-gray-50 px-4 py-3 text-sm font-semibold text-text-primary shadow-sm transition hover:bg-gray-100">
<span className="flex cursor-pointer items-center gap-3">
<input
type="checkbox"
className="h-4 w-4 rounded border-gray-300 text-primary-blue focus:ring-primary-blue"
checked={formSetActive}
onChange={(event) => setFormSetActive(event.target.checked)}
/>
Set active directly
</span>
</label>
</div>
<div>
<label className="mb-2 block text-xs font-bold uppercase tracking-[0.1em] text-text-muted">cURL Data</label>
<div className="group overflow-hidden rounded-2xl border border-gray-800 bg-gray-900 shadow-inner">
<textarea
value={formCurl}
onChange={(event) => setFormCurl(event.target.value)}
placeholder="curl --request POST --url ..."
className="h-64 w-full resize-none bg-transparent px-5 py-5 font-mono text-xs leading-relaxed text-gray-100 outline-none transition placeholder:text-gray-600 focus:bg-gray-950"
required
spellCheck="false"
/>
</div>
</div>
<div className="flex gap-3 border-t border-gray-100 pt-6">
<button
type="button"
onClick={() => setShowImportModal(false)}
disabled={saving}
className="flex-1 rounded-xl border border-gray-200 bg-white px-4 py-3 text-sm font-bold text-gray-600 shadow-sm transition hover:bg-gray-50 disabled:opacity-50"
>
Cancel
</button>
<button
type="submit"
disabled={saving}
className="flex-1 rounded-xl bg-primary-blue px-4 py-3 text-sm font-bold text-white shadow-sm transition hover:bg-primary-dark disabled:opacity-50"
>
{saving ? 'Validating…' : 'Import'}
</button>
</div>
</form>
</div>
</div>
</div>
)}
<div className="flex h-[calc(100vh-[var(--header-height,64px)])] min-h-[700px] w-full border-t border-gray-200 bg-white font-sans text-[13px] text-gray-800">
<aside className="flex w-[280px] shrink-0 flex-col border-r border-gray-200 bg-gray-50/30 pt-1">
<div className="flex-1 overflow-y-auto px-2 space-y-4">
<button
type="button"
onClick={() => {
setFormCurl('');
setShowImportModal(true);
}}
className="w-full rounded px-2 py-1.5 text-left text-gray-600 transition hover:bg-gray-100"
>
<span className="flex items-center gap-2">
<svg className="h-3.5 w-3.5 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M12 4v16m8-8H4" />
</svg>
<span className="text-xs font-medium">New Request</span>
</span>
</button>
<div>
<div className="flex cursor-pointer items-center gap-2 rounded px-2 py-1.5 text-[11px] font-semibold text-gray-500 transition hover:bg-gray-100/50">
<svg className="h-3.5 w-3.5 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M19 9l-7 7-7-7" />
</svg>
Saved Profiles
</div>
<div className="space-y-0.5 py-1 pl-5 pr-1">
{profiles.map((profile) => {
const isSelected = profile.id === selectedProfile?.id;
const isActive = profile.id === activeProfileId;
const needsFields = profile.executionReadiness?.isSetupComplete !== true;
return (
<button
key={profile.id}
type="button"
onClick={() => setSelectedProfileId(profile.id)}
className={`group flex w-full items-center gap-2.5 rounded px-2 py-1.5 text-left transition ${
isSelected ? 'bg-blue-50/70 font-medium text-blue-800' : 'text-gray-700 hover:bg-gray-100'
}`}
>
<span className={`text-[10px] font-bold ${isSelected ? 'text-green-600' : 'text-green-600/80 group-hover:text-green-600'}`}>POST</span>
<span className="truncate text-xs">{profile.name || profile.provider?.providerName || 'Unknown Request'}</span>
{isActive && (
<div
className="ml-auto h-1.5 w-1.5 rounded-full bg-blue-500 shadow-[0_0_4px_rgba(59,130,246,0.5)]"
title="Active Delivery Profile"
/>
)}
{!isActive && needsFields && (
<div
className="ml-auto h-1.5 w-1.5 rounded-full bg-orange-400"
title="Needs Required Inputs"
/>
)}
</button>
);
})}
{profiles.length === 0 && (
<div className="px-2 py-2 text-xs text-gray-400">No requests saved yet.</div>
)}
</div>
</div>
</div>
</aside>
<main className="relative flex min-w-0 flex-1 flex-col bg-white">
<div className="absolute right-4 top-2 z-10 flex gap-2">
{error && <span className="rounded border border-red-200 bg-red-50 px-3 py-1 text-xs font-medium text-red-600">Error: {error}</span>}
{success && <span className="rounded border border-green-200 bg-green-50 px-3 py-1 text-xs font-medium text-green-700">{success}</span>}
{refreshing && <span className="px-3 py-1 text-xs font-medium text-gray-500">Syncing...</span>}
</div>
<div className="group flex items-center gap-3 border-b border-gray-200 bg-white px-4 py-4">
<div className="flex flex-1 items-center rounded border border-gray-300 bg-gray-50/50 px-1.5 py-1 transition focus-within:border-gray-400 focus-within:shadow-[0_0_0_2px_rgba(200,200,200,0.2)]">
<select
disabled={!!selectedProfile}
value={selectedRequestDraft?.method || 'POST'}
className="cursor-pointer appearance-none bg-transparent px-2 py-1 text-[13px] font-bold text-green-600 outline-none"
>
<option>POST</option>
<option>GET</option>
</select>
<div className="mx-2 h-5 w-px bg-gray-200" />
{!selectedProfile ? (
<input
value={formCurl}
onChange={(event) => setFormCurl(event.target.value)}
className="flex-1 bg-transparent px-2 py-1 font-mono text-[13px] text-gray-800 outline-none placeholder:text-gray-400"
placeholder="Enter URL or paste raw cURL text here..."
spellCheck="false"
/>
) : (
<input
ref={urlEditorRef}
readOnly={editingUrlProfileId !== selectedProfile.id}
value={selectedRequestUrl}
onClick={handleBeginUrlEdit}
onChange={(event) => updateRequestDraft(selectedProfile.id, (current) => ({
...current,
url: event.target.value,
urlMasked: false,
}))}
onBlur={() => setEditingUrlProfileId('')}
onKeyDown={(event) => {
if (event.key === 'Enter') {
event.currentTarget.blur();
}
if (event.key === 'Escape') {
updateRequestDraft(selectedProfile.id, selectedSavedRequest);
setEditingUrlProfileId('');
}
}}
className="flex-1 cursor-text bg-transparent px-2 py-1 font-mono text-[13px] text-gray-800 outline-none"
/>
)}
</div>
{(!selectedProfile || hasPendingProfileChanges) && (
<button
type="button"
onClick={handleValidateAndSave}
disabled={saving || savingInputs || (!selectedProfile && !formCurl.trim())}
className="flex min-w-[80px] items-center justify-center rounded-[4px] bg-[#0066cc] px-5 py-[7px] text-sm font-medium text-white transition hover:bg-[#0052a3] disabled:opacity-50"
>
{(saving || savingInputs) ? '...' : (selectedProfile ? 'Save' : 'Import')}
</button>
)}
{selectedProfile && (
<div className="ml-1 flex items-center gap-1.5">
<button
type="button"
onClick={() => handleCopyCurl(selectedProfile)}
className="rounded p-1.5 text-gray-500 transition hover:bg-gray-100 hover:text-gray-800"
title="Copy Raw cURL"
>
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
</svg>
</button>
<button
type="button"
onClick={() => handleDeleteRequest(selectedProfile)}
className="rounded p-1.5 text-gray-500 transition hover:bg-red-50 hover:text-red-600"
title="Delete Profile"
>
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
</button>
{!isSelectedProfileActive && <div className="mx-1 h-5 w-px bg-gray-200" />}
{!isSelectedProfileActive && (
<button
type="button"
onClick={() => handleActivate(selectedProfile.id)}
className="ml-1 rounded bg-gray-100 px-3 py-1.5 text-xs font-semibold text-gray-600 transition hover:bg-gray-200 hover:text-gray-900"
>
Make Active
</button>
)}
</div>
)}
</div>
<div className="flex items-center border-b border-gray-200 px-5 text-[13px]">
{WORKSPACE_TABS.map((tab) => (
<button
key={tab.id}
type="button"
onClick={() => setWorkspaceTab(tab.id)}
className={`border-b-[3px] px-3 py-2.5 font-medium transition ${
workspaceTab === tab.id ? 'border-orange-500 text-gray-900' : 'border-transparent text-gray-500 hover:text-gray-800'
}`}
>
{tab.label}
{tab.id === 'headers' && <span className="ml-1 font-normal text-gray-400">({selectedRequestDraft?.headers?.length || 0})</span>}
</button>
))}
</div>
<div className="flex flex-1 flex-col overflow-y-auto bg-white">
{workspaceTab === 'authorization' && (
<div className="flex h-full text-[13px]">
<div className="w-64 shrink-0 border-r border-gray-200 p-4">
<label className="mb-2 block font-bold text-gray-700">Auth Type</label>
<div className="w-full rounded-[3px] border border-gray-300 bg-gray-50 px-3 py-1.5 text-[13px] text-gray-800">
{selectedProfile ? formatAuthMode(selectedProfile.curlAnalysis?.authMode) : 'No Auth'}
</div>
<p className="mt-8 pr-2 text-[12px] leading-relaxed text-gray-500">
Review and edit the saved authorization and setup values for this request. Runtime fields do not belong in this step.
</p>
</div>
<div className="flex-1 p-6">
{authorizationInputs.map((input) => {
const isError = attemptedSave && missingInputKeys.has(input.key) && !String(inputForm[input.key] || '').trim();
const isVisible = inputVisibility[input.key] === true;
const draftValue = String(inputForm[input.key] ?? '');
const revealedValue = String(selectedRevealProfileInputs[input.key]?.value ?? input.value ?? '');
const displayValue = input.secret
? (
isVisible
? (draftValue || revealedValue)
: (draftValue || input.maskedValue || '')
)
: (draftValue || input.value || '');
const isReadOnly = input.secret && !isVisible && input.hasValue === true && !draftValue;
return (
<div key={input.key} className="mb-4 flex max-w-2xl items-center">
<span className="w-32 shrink-0 font-medium text-gray-700">{input.label || 'Value'}</span>
<div className={`relative flex-1 overflow-hidden rounded-[3px] transition ${isError ? 'ring-1 ring-red-400' : 'ring-1 ring-amber-400/80 focus-within:ring-amber-500'}`}>
<input
type={input.secret && !isVisible ? 'password' : 'text'}
readOnly={isReadOnly}
value={displayValue}
onChange={(event) => {
setAttemptedSave(false);
setInputForm((current) => ({ ...current, [input.key]: event.target.value }));
}}
className={`w-full px-3 py-1.5 pr-10 font-mono text-[13px] outline-none ${isError ? 'bg-red-50 text-red-900' : 'bg-white text-gray-800'}`}
placeholder={isError ? 'Required' : ''}
/>
<div className="absolute right-2 top-0 bottom-0 flex items-center gap-1.5">
{input.secret && (
<button
type="button"
onClick={() => handleToggleInputVisibility(input)}
className="text-gray-400 transition hover:text-gray-700"
tabIndex="-1"
>
{isVisible ? (
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21" />
</svg>
) : (
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
<path strokeLinecap="round" strokeLinejoin="round" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
</svg>
)}
</button>
)}
{isError && (
<svg className="h-4 w-4 text-amber-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
)}
</div>
</div>
</div>
);
})}
{!selectedProfile && (
<div className="italic text-gray-400">Select or import a profile to manage Authorization.</div>
)}
{selectedProfile && authorizationInputs.length === 0 && (
<div className="italic text-gray-400">No saved authorization or setup values are defined for this request.</div>
)}
</div>
</div>
)}
{workspaceTab === 'headers' && (
<div className="flex w-full flex-col">
<div className="flex items-center gap-3 border-b border-gray-100 bg-gray-50/30 px-5 py-2">
<span className="text-[13px] font-bold text-gray-800">Headers</span>
</div>
<table className="table-fixed w-full border-collapse text-left">
<thead>
<tr className="border-b border-gray-200 text-[12px] font-semibold text-gray-500">
<th className="w-[40px] px-3 py-2 text-center"></th>
<th className="w-[30%] border-l border-gray-100 px-4 py-2">Key</th>
<th className="w-[45%] border-l border-gray-100 px-4 py-2">Value</th>
<th className="w-[20%] border-l border-gray-100 px-4 py-2">Description</th>
</tr>
</thead>
<tbody>
{selectedRequestDraft?.headers.map((header, index) => {
const savedHeader = selectedSavedRequest?.headers.find((candidate) => candidate.id === header.id) || selectedSavedRequest?.headers[index];
const isEditingKey = editingHeader?.profileId === selectedProfile?.id && editingHeader?.headerId === header.id && editingHeader?.field === 'key';
const isEditingValue = editingHeader?.profileId === selectedProfile?.id && editingHeader?.headerId === header.id && editingHeader?.field === 'value';
const isHeaderVisible = selectedHeaderVisibility[header.id] === true;
const displayValue = header.masked && !isEditingValue && !isHeaderVisible ? (header.maskedValue || header.value) : header.value;
return (
<tr key={header.id} className="group border-b border-gray-100 hover:bg-gray-50/30">
<td className="px-3 py-2 text-center align-middle">
<input type="checkbox" checked={header.enabled !== false} readOnly className="pointer-events-none rounded-[3px] border-gray-300 accent-blue-600" />
</td>
<td className="border-l border-gray-100 px-4 py-2 align-middle">
{isEditingKey ? (
<input
autoFocus
type="text"
value={header.key}
onChange={(event) => updateRequestDraft(selectedProfile.id, (current) => ({
...current,
headers: current.headers.map((entry) => (
entry.id === header.id
? { ...entry, key: event.target.value }
: entry
)),
}))}
onBlur={() => setEditingHeader(null)}
onKeyDown={(event) => {
if (event.key === 'Enter') {
event.currentTarget.blur();
}
if (event.key === 'Escape') {
updateRequestDraft(selectedProfile.id, (current) => ({
...current,
headers: current.headers.map((entry) => (
entry.id === header.id
? { ...entry, key: savedHeader?.key || entry.key }
: entry
)),
}));
setEditingHeader(null);
}
}}
className="w-full bg-transparent px-0 py-0 font-mono text-[13px] text-gray-800 outline-none"
/>
) : (
<button type="button" onClick={() => handleBeginHeaderEdit(header.id, 'key', header)} className="flex w-full items-center gap-1 text-left">
<span className="font-mono text-[13px] text-gray-800">{header.key}</span>
</button>
)}
</td>
<td className="border-l border-gray-100 px-4 py-2 align-middle">
<div className="relative flex items-center">
{isEditingValue ? (
<input
autoFocus
type={header.secret && !isHeaderVisible ? 'password' : 'text'}
value={header.value}
onChange={(event) => updateRequestDraft(selectedProfile.id, (current) => ({
...current,
headers: current.headers.map((entry) => (
entry.id === header.id
? { ...entry, value: event.target.value, masked: false }
: entry
)),
}))}
onBlur={() => setEditingHeader(null)}
onKeyDown={(event) => {
if (event.key === 'Enter') {
event.currentTarget.blur();
}
if (event.key === 'Escape') {
updateRequestDraft(selectedProfile.id, (current) => ({
...current,
headers: current.headers.map((entry) => (
entry.id === header.id
? {
...entry,
value: savedHeader?.value || entry.value,
masked: savedHeader?.masked === true,
maskedValue: savedHeader?.maskedValue || entry.maskedValue,
}
: entry
)),
}));
setEditingHeader(null);
}
}}
className="w-full bg-transparent px-0 py-0 font-mono text-[13px] text-gray-800 outline-none"
/>
) : (
<button type="button" onClick={() => handleBeginHeaderEdit(header.id, 'value', header)} className="flex-1 text-left">
<span className={`font-mono text-[13px] ${header.secret ? 'select-none text-gray-500' : 'text-gray-800'}`}>{displayValue}</span>
</button>
)}
{(header.masked || isHeaderVisible) && (
<button
type="button"
onClick={() => handleToggleHeaderReveal(selectedProfile, header)}
className="ml-2 rounded p-1 text-gray-400 opacity-0 transition hover:text-gray-700 group-hover:opacity-100"
title={isHeaderVisible ? 'Hide' : 'Reveal'}
>
{isHeaderVisible ? (
<svg className="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21" />
</svg>
) : (
<svg className="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
<path strokeLinecap="round" strokeLinejoin="round" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
</svg>
)}
</button>
)}
</div>
</td>
<td className="border-l border-gray-100 px-4 py-2 align-middle"></td>
</tr>
);
})}
{!selectedProfile && (
<tr><td colSpan={4} className="px-4 py-6 text-center text-[13px] italic text-gray-400">No profile loaded.</td></tr>
)}
{selectedProfile && (selectedRequestDraft?.headers?.length || 0) === 0 && (
<tr><td colSpan={4} className="px-4 py-6 text-center text-[13px] italic text-gray-400">No headers detected.</td></tr>
)}
</tbody>
</table>
</div>
)}
{workspaceTab === 'curl' && (
<div className="h-full p-4">
{selectedProfile ? (
<div className="flex h-full flex-col overflow-hidden rounded border border-gray-200 bg-white">
<div className="flex items-center justify-between border-b border-gray-200 bg-gray-50 px-3 py-1.5 font-mono text-[11px] text-gray-500 shadow-sm">
<span className="pl-1">1</span>
<button type="button" onClick={() => handleToggleCurl(selectedProfile)} className="transition hover:text-gray-800">
{isSelectedCurlVisible ? 'Hide Secrets' : 'Reveal Data'}
</button>
</div>
<pre className="flex-1 overflow-auto whitespace-pre-wrap break-all p-4 font-mono text-[12px] leading-relaxed text-gray-700">
{beautifyCurl(selectedDisplayCurl)}
</pre>
</div>
) : (
<div className="px-2 py-2 text-[13px] italic text-gray-400">Paste your cURL in the URL bar above and click Import to create a request.</div>
)}
</div>
)}
{workspaceTab === 'body' && (
<div className="h-full p-4">
{selectedProfile ? (
<div className="flex h-full flex-col overflow-hidden rounded border border-gray-200 bg-white">
<div className="flex items-center justify-between border-b border-gray-200 bg-gray-50 px-3 py-1.5 font-mono text-[11px] text-gray-500 shadow-sm">
<span className="pl-1">{selectedBodyPreview.label}</span>
</div>
<pre className="flex-1 overflow-auto whitespace-pre-wrap break-all p-4 font-mono text-[12px] leading-relaxed text-gray-700">
{selectedBodyPreview.content || 'No request body detected.'}
</pre>
</div>
) : (
<div className="px-2 py-2 text-[13px] italic text-gray-400">
Paste your cURL in the URL bar above and click Import to preview the request body.
</div>
)}
</div>
)}
</div>
</main>
</div>
</>
);
}