diff --git a/internal/portal/src/common/Checkbox/Checkbox.scss b/internal/portal/src/common/Checkbox/Checkbox.scss index f76105ba..5074d700 100644 --- a/internal/portal/src/common/Checkbox/Checkbox.scss +++ b/internal/portal/src/common/Checkbox/Checkbox.scss @@ -33,8 +33,21 @@ } &:disabled ~ .checkbox__checkmark { - background-color: var(--disabled-bg); - border-color: var(--disabled-border); + background-color: var( + --disabled-bg, + var(--colors-background-neutral-subtle) + ); + border-color: var(--disabled-border, var(--colors-border-neutral-subtle)); + cursor: not-allowed; + opacity: 0.6; + } + + &:checked:disabled ~ .checkbox__checkmark { + background-color: var(--colors-background-primary); + opacity: 0.6; + } + + &:disabled { cursor: not-allowed; } } @@ -87,6 +100,14 @@ } } + &:has(input:disabled) { + cursor: not-allowed; + + .checkbox__label { + opacity: 0.6; + } + } + &__error { position: absolute; bottom: -20px; diff --git a/internal/portal/src/common/TopicPicker/TopicPicker.scss b/internal/portal/src/common/TopicPicker/TopicPicker.scss index 062d394f..70b635ad 100644 --- a/internal/portal/src/common/TopicPicker/TopicPicker.scss +++ b/internal/portal/src/common/TopicPicker/TopicPicker.scss @@ -1,11 +1,10 @@ .topic-picker { border: 1px solid var(--colors-outline-neutral); border-radius: var(--radius-m); - max-height: 100%; - height: 100%; display: flex; flex-direction: column; overflow: hidden; + max-height: 100%; &__header { border-bottom: 1px solid var(--colors-outline-neutral); diff --git a/internal/portal/src/common/TopicPicker/TopicPicker.tsx b/internal/portal/src/common/TopicPicker/TopicPicker.tsx index 1e08afa3..78954dfe 100644 --- a/internal/portal/src/common/TopicPicker/TopicPicker.tsx +++ b/internal/portal/src/common/TopicPicker/TopicPicker.tsx @@ -19,30 +19,32 @@ interface TopicPickerProps { const detectSeparator = (topics: string[]): string => { // Common separators to check - const possibleSeparators = ['/', '.', '-']; - + const possibleSeparators = ["/", ".", "-"]; + // Find the first separator that appears in all topics // and is the first occurring separator in each topic - return possibleSeparators.find(sep => - topics.every(topic => { - const sepIndex = topic.indexOf(sep); - if (sepIndex === -1) return false; - - // Check if any other separator appears before this one - const otherSepsIndex = possibleSeparators - .filter(s => s !== sep) - .map(s => topic.indexOf(s)) - .filter(idx => idx !== -1); - - return otherSepsIndex.every(idx => idx === -1 || idx > sepIndex); - }) - ) || '-'; // Fallback to '-' if no consistent separator is found + return ( + possibleSeparators.find((sep) => + topics.every((topic) => { + const sepIndex = topic.indexOf(sep); + if (sepIndex === -1) return false; + + // Check if any other separator appears before this one + const otherSepsIndex = possibleSeparators + .filter((s) => s !== sep) + .map((s) => topic.indexOf(s)) + .filter((idx) => idx !== -1); + + return otherSepsIndex.every((idx) => idx === -1 || idx > sepIndex); + }) + ) || "-" + ); // Fallback to '-' if no consistent separator is found }; const topics: Topic[] = (() => { const topicsList = CONFIGS.TOPICS.split(","); const separator = detectSeparator(topicsList); - + return topicsList.map((topic) => { const parts = topic.split(separator); return { @@ -174,9 +176,10 @@ const TopicPicker = ({ .split(" ") .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) .join(" ")}`} - checked={areAllSelected} - indeterminate={isIndeterminate} + checked={isEverythingSelected || areAllSelected} + indeterminate={!isEverythingSelected && isIndeterminate} onChange={() => toggleCategorySelection(categoryTopics)} + disabled={isEverythingSelected} /> {isExpanded && ( @@ -184,10 +187,14 @@ const TopicPicker = ({ {categoryTopics.map((topic) => (
toggleTopic(topic.id)} label={topic.id} monospace + disabled={isEverythingSelected} />
))} diff --git a/internal/portal/src/scenes/CreateDestination/CreateDestination.scss b/internal/portal/src/scenes/CreateDestination/CreateDestination.scss index 7095b1dc..1aeac43d 100644 --- a/internal/portal/src/scenes/CreateDestination/CreateDestination.scss +++ b/internal/portal/src/scenes/CreateDestination/CreateDestination.scss @@ -3,10 +3,10 @@ grid-template-columns: 272px 1fr; width: 100%; gap: 132px; - padding-top: calc(var(--base-grid-multiplier) * 18); margin: 0 auto; flex-direction: row; - height: 100vh; + padding-top: calc(var(--base-grid-multiplier) * 18); + height: calc(100vh - calc(var(--base-grid-multiplier) * 18)); &__sidebar { display: flex; @@ -55,16 +55,23 @@ } &__step { + height: 100%; + overflow: hidden; display: flex; flex-direction: column; padding-bottom: var(--spacing-16); box-sizing: border-box; - h1 { - margin-top: 64px; - margin-bottom: 0; - ~ p { - margin-top: 0; + &__header { + padding-left: 4px; + padding-right: 4px; + + h1 { + margin-top: 64px; + margin-bottom: 0; + ~ p { + margin-top: 0; + } } } @@ -72,41 +79,55 @@ display: flex; flex-direction: column; flex: 1; + min-height: 0; + + > div { + padding: 4px; + } } &__fields { flex: 0 1 auto; - max-height: 100%; + min-height: 0; } &__actions { margin-top: var(--spacing-8); display: flex; justify-content: flex-end; + flex-shrink: 0; } } } .destination-types { - display: flex; - flex-direction: column; - gap: 1rem; + border: 1px solid var(--colors-outline-neutral); + border-radius: var(--radius-m); + overflow: hidden; + height: 100%; + + &__container { + overflow-y: auto; + display: flex; + flex-direction: column; + } } -.destination-type-card { +.destination-type-option { cursor: pointer; position: relative; - border: 1px solid var(--colors-outline-neutral); - border-radius: var(--radius-m); + border-bottom: 1px solid var(--colors-outline-neutral); + + &:last-child { + border-bottom: none; + } &:has(input[type="radio"]:checked) { - border-color: var(--colors-outline-primary-focus); - outline: 1px solid var(--colors-outline-primary-focus); + background-color: var(--colors-background-hover); } &:hover { - border-color: var(--colors-outline-primary-focus); - outline: 1px solid var(--colors-outline-primary-focus); + background-color: var(--colors-background-hover); } input[type="radio"] { diff --git a/internal/portal/src/scenes/CreateDestination/CreateDestination.tsx b/internal/portal/src/scenes/CreateDestination/CreateDestination.tsx index 64e8e3ba..8a831de0 100644 --- a/internal/portal/src/scenes/CreateDestination/CreateDestination.tsx +++ b/internal/portal/src/scenes/CreateDestination/CreateDestination.tsx @@ -21,6 +21,7 @@ type Step = { FormFields: (props: { defaultValue: Record; onChange: (value: Record) => void; + destinations?: DestinationTypeReference[]; }) => React.ReactNode; action: string; }; @@ -84,35 +85,39 @@ const DESTINATION_TYPE_STEP: Step = { FormFields: ({ destinations, defaultValue, + onChange, }: { - destinations: DestinationTypeReference[]; + destinations?: DestinationTypeReference[]; defaultValue: Record; + onChange?: (value: Record) => void; }) => (
- {destinations?.map((destination) => ( - - ))} +
+ {destinations?.map((destination) => ( + + ))} +
), action: "Next", @@ -122,12 +127,18 @@ const CONFIGURATION_STEP: Step = { title: "Configure destination", sidebar_shortname: "Configure destination", description: "Configure the destination you want to send to your destination", + isValid: (values: Record) => { + // Form validation will be handled by the form's native validation + return true; + }, FormFields: ({ defaultValue, destinations, + onChange, }: { defaultValue: Record; - destinations: DestinationTypeReference[]; + destinations?: DestinationTypeReference[]; + onChange?: (value: Record) => void; }) => { const destinationType = destinations?.find( (d) => d.type === defaultValue.type @@ -164,6 +175,15 @@ export default function CreateDestination() { const currentStep = steps[currentStepIndex]; const nextStep = steps[currentStepIndex + 1] || null; + // Validate the current step when it changes or stepValues change + useEffect(() => { + if (currentStep.isValid) { + setIsValid(currentStep.isValid(stepValues)); + } else { + setIsValid(false); + } + }, [currentStepIndex, stepValues, currentStep]); + const createDestination = (values: Record) => { setIsCreating(true); @@ -244,16 +264,19 @@ export default function CreateDestination() {
-

{currentStep.title}

-

{currentStep.description}

+
+

{currentStep.title}

+

{currentStep.description}

+
{ const formData = new FormData(e.currentTarget); const values = Object.fromEntries(formData.entries()); + const allValues = { ...stepValues, ...values }; if (currentStep.isValid) { - setIsValid(currentStep.isValid(values)); + setIsValid(currentStep.isValid(allValues)); } else { setIsValid(e.currentTarget.checkValidity()); }