mirror of
https://github.com/n8n-io/n8n.git
synced 2026-10-12 02:19:43 +00:00
refactor(editor): Centralize instance AI confirmation dialog styling (#28195)
Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
a95cbfb429
commit
cc32c507c5
+44
@@ -0,0 +1,44 @@
|
||||
<script lang="ts" setup>
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
layout?: 'row-end' | 'row-between' | 'column';
|
||||
bordered?: boolean;
|
||||
}>(),
|
||||
{
|
||||
layout: 'row-end',
|
||||
bordered: false,
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="[$style.footer, $style[layout], bordered && $style.bordered]">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" module>
|
||||
.footer {
|
||||
display: flex;
|
||||
padding: var(--spacing--xs) var(--spacing--sm);
|
||||
}
|
||||
|
||||
.row-end {
|
||||
gap: var(--spacing--2xs);
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.row-between {
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.column {
|
||||
flex-direction: column;
|
||||
gap: var(--spacing--2xs);
|
||||
}
|
||||
|
||||
.bordered {
|
||||
border-top: var(--border);
|
||||
}
|
||||
</style>
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
<template>
|
||||
<div :class="$style.preview">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" module>
|
||||
.preview {
|
||||
font-family: monospace;
|
||||
font-size: var(--font-size--sm);
|
||||
color: var(--color--text);
|
||||
word-break: break-all;
|
||||
padding: var(--spacing--2xs);
|
||||
background: var(--color--background);
|
||||
border-radius: var(--radius);
|
||||
border: var(--border);
|
||||
}
|
||||
</style>
|
||||
+23
-87
@@ -1,9 +1,12 @@
|
||||
<script lang="ts" setup>
|
||||
import { N8nActionDropdown, N8nButton, N8nIconButton } from '@n8n/design-system';
|
||||
import { N8nButton, N8nText } from '@n8n/design-system';
|
||||
import type { ActionDropdownItem } from '@n8n/design-system/types';
|
||||
import { useI18n } from '@n8n/i18n';
|
||||
import { computed, ref } from 'vue';
|
||||
import { useInstanceAiStore } from '../instanceAi.store';
|
||||
import ConfirmationFooter from './ConfirmationFooter.vue';
|
||||
import ConfirmationPreview from './ConfirmationPreview.vue';
|
||||
import SplitButton from './SplitButton.vue';
|
||||
|
||||
type DomainAction = 'allow_once' | 'allow_domain' | 'allow_all';
|
||||
|
||||
@@ -64,7 +67,7 @@ function onPrimaryClick() {
|
||||
handleAction(true, primaryAction.value);
|
||||
}
|
||||
|
||||
function onDropdownSelect(action: DomainAction) {
|
||||
function onDropdownSelect(action: string) {
|
||||
handleAction(true, action);
|
||||
}
|
||||
</script>
|
||||
@@ -72,15 +75,15 @@ function onDropdownSelect(action: DomainAction) {
|
||||
<template>
|
||||
<div v-if="!resolved">
|
||||
<div :class="$style.body">
|
||||
<div :class="$style.message">
|
||||
<span>{{
|
||||
<N8nText tag="div" size="medium" bold>
|
||||
{{
|
||||
i18n.baseText('instanceAi.domainAccess.prompt', { interpolate: { domain: props.host } })
|
||||
}}</span>
|
||||
</div>
|
||||
<div :class="$style.urlPreview">{{ props.url }}</div>
|
||||
}}
|
||||
</N8nText>
|
||||
<ConfirmationPreview>{{ props.url }}</ConfirmationPreview>
|
||||
</div>
|
||||
|
||||
<div :class="$style.actions">
|
||||
<ConfirmationFooter>
|
||||
<N8nButton
|
||||
variant="outline"
|
||||
size="small"
|
||||
@@ -88,92 +91,25 @@ function onDropdownSelect(action: DomainAction) {
|
||||
data-test-id="domain-access-deny"
|
||||
@click="handleAction(false)"
|
||||
/>
|
||||
<div :class="$style.splitButton">
|
||||
<N8nButton
|
||||
:variant="isDestructive ? 'destructive' : 'solid'"
|
||||
:class="$style.splitButtonMain"
|
||||
:label="primaryLabel"
|
||||
data-test-id="domain-access-primary"
|
||||
size="small"
|
||||
@click="onPrimaryClick"
|
||||
/>
|
||||
<N8nActionDropdown
|
||||
:items="dropdownItems"
|
||||
:class="$style.splitButtonDropdown"
|
||||
data-test-id="domain-access-dropdown"
|
||||
placement="bottom-start"
|
||||
@select="onDropdownSelect"
|
||||
>
|
||||
<template #activator>
|
||||
<N8nIconButton
|
||||
:variant="isDestructive ? 'destructive' : 'solid'"
|
||||
icon="chevron-down"
|
||||
:class="$style.splitButtonCaret"
|
||||
aria-label="More approval options"
|
||||
size="small"
|
||||
/>
|
||||
</template>
|
||||
</N8nActionDropdown>
|
||||
</div>
|
||||
</div>
|
||||
<SplitButton
|
||||
:variant="isDestructive ? 'destructive' : 'solid'"
|
||||
:label="primaryLabel"
|
||||
:items="dropdownItems"
|
||||
data-test-id="domain-access-primary"
|
||||
dropdown-test-id="domain-access-dropdown"
|
||||
caret-aria-label="More approval options"
|
||||
@click="onPrimaryClick"
|
||||
@select="onDropdownSelect"
|
||||
/>
|
||||
</ConfirmationFooter>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" module>
|
||||
.message {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: var(--spacing--3xs);
|
||||
font-size: var(--font-size--2xs);
|
||||
color: var(--color--text);
|
||||
margin-bottom: var(--spacing--xs);
|
||||
font-weight: var(--font-weight--medium);
|
||||
}
|
||||
|
||||
.urlPreview {
|
||||
font-family: monospace;
|
||||
font-size: var(--font-size--3xs);
|
||||
color: var(--color--text--tint-1);
|
||||
word-break: break-all;
|
||||
margin-bottom: var(--spacing--xs);
|
||||
padding: var(--spacing--2xs);
|
||||
background: var(--color--background);
|
||||
border-radius: var(--radius);
|
||||
border: var(--border);
|
||||
}
|
||||
|
||||
.body {
|
||||
padding: var(--spacing--sm) var(--spacing--sm);
|
||||
padding: var(--spacing--sm) var(--spacing--sm) 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing--3xs);
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: var(--spacing--2xs);
|
||||
justify-content: flex-end;
|
||||
border-top: var(--border);
|
||||
padding: var(--spacing--xs) var(--spacing--sm);
|
||||
}
|
||||
|
||||
.splitButton {
|
||||
display: flex;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.splitButtonMain {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
.splitButtonDropdown {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.splitButtonCaret {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
border-left: 1px solid var(--color--foreground--tint-2);
|
||||
}
|
||||
</style>
|
||||
|
||||
+18
-110
@@ -1,5 +1,5 @@
|
||||
<script lang="ts" setup>
|
||||
import { N8nActionDropdown, N8nButton, N8nIconButton } from '@n8n/design-system';
|
||||
import { N8nButton, N8nText } from '@n8n/design-system';
|
||||
import type { ActionDropdownItem } from '@n8n/design-system/types';
|
||||
import { useI18n } from '@n8n/i18n';
|
||||
import { computed } from 'vue';
|
||||
@@ -7,6 +7,9 @@ import { computed } from 'vue';
|
||||
import { useTelemetry } from '@/app/composables/useTelemetry';
|
||||
import { useRootStore } from '@n8n/stores/useRootStore';
|
||||
import { useInstanceAiStore } from '../instanceAi.store';
|
||||
import ConfirmationFooter from './ConfirmationFooter.vue';
|
||||
import ConfirmationPreview from './ConfirmationPreview.vue';
|
||||
import SplitButton from './SplitButton.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
requestId: string;
|
||||
@@ -90,17 +93,17 @@ async function confirm(decision: string) {
|
||||
<template>
|
||||
<div :class="$style.root">
|
||||
<div :class="$style.body">
|
||||
<div :class="$style.message">
|
||||
<N8nText tag="div" size="medium" bold>
|
||||
{{
|
||||
i18n.baseText('instanceAi.gatewayConfirmation.prompt', {
|
||||
interpolate: { resources: props.resource },
|
||||
})
|
||||
}}
|
||||
</div>
|
||||
<div :class="$style.preview">{{ props.description }}</div>
|
||||
</N8nText>
|
||||
<ConfirmationPreview>{{ props.description }}</ConfirmationPreview>
|
||||
</div>
|
||||
|
||||
<div :class="$style.actions">
|
||||
<ConfirmationFooter>
|
||||
<!-- Unknown options not in the standard set -->
|
||||
<N8nButton
|
||||
v-for="opt in otherOptions"
|
||||
@@ -113,80 +116,30 @@ async function confirm(decision: string) {
|
||||
|
||||
<!-- Deny side -->
|
||||
<template v-if="denyPrimary">
|
||||
<div v-if="denyDropdownItems.length" :class="$style.splitButton">
|
||||
<N8nButton
|
||||
variant="outline"
|
||||
size="small"
|
||||
:label="denyPrimary.label"
|
||||
:class="$style.splitButtonMain"
|
||||
data-test-id="gateway-decision-deny"
|
||||
@click="confirm(denyPrimary.decision)"
|
||||
/>
|
||||
<N8nActionDropdown
|
||||
:items="denyDropdownItems"
|
||||
:class="$style.splitButtonDropdown"
|
||||
placement="bottom-start"
|
||||
@select="confirm"
|
||||
>
|
||||
<template #activator>
|
||||
<N8nIconButton
|
||||
variant="outline"
|
||||
icon="chevron-down"
|
||||
:class="$style.splitButtonCaret"
|
||||
aria-label="More deny options"
|
||||
size="small"
|
||||
/>
|
||||
</template>
|
||||
</N8nActionDropdown>
|
||||
</div>
|
||||
<N8nButton
|
||||
v-else
|
||||
<SplitButton
|
||||
variant="outline"
|
||||
size="small"
|
||||
:label="denyPrimary.label"
|
||||
:items="denyDropdownItems"
|
||||
data-test-id="gateway-decision-deny"
|
||||
caret-aria-label="More deny options"
|
||||
@click="confirm(denyPrimary.decision)"
|
||||
@select="confirm"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- Approve side -->
|
||||
<template v-if="approvePrimary">
|
||||
<div v-if="approveDropdownItems.length" :class="$style.splitButton">
|
||||
<N8nButton
|
||||
variant="solid"
|
||||
size="small"
|
||||
:label="approvePrimary.label"
|
||||
:class="$style.splitButtonMain"
|
||||
data-test-id="gateway-decision-approve"
|
||||
@click="confirm(approvePrimary.decision)"
|
||||
/>
|
||||
<N8nActionDropdown
|
||||
:items="approveDropdownItems"
|
||||
:class="$style.splitButtonDropdown"
|
||||
placement="bottom-start"
|
||||
@select="confirm"
|
||||
>
|
||||
<template #activator>
|
||||
<N8nIconButton
|
||||
variant="solid"
|
||||
icon="chevron-down"
|
||||
:class="$style.splitButtonCaret"
|
||||
aria-label="More approve options"
|
||||
size="small"
|
||||
/>
|
||||
</template>
|
||||
</N8nActionDropdown>
|
||||
</div>
|
||||
<N8nButton
|
||||
v-else
|
||||
<SplitButton
|
||||
variant="solid"
|
||||
size="small"
|
||||
:label="approvePrimary.label"
|
||||
:items="approveDropdownItems"
|
||||
data-test-id="gateway-decision-approve"
|
||||
caret-aria-label="More approve options"
|
||||
@click="confirm(approvePrimary.decision)"
|
||||
@select="confirm"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</ConfirmationFooter>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -197,54 +150,9 @@ async function confirm(decision: string) {
|
||||
}
|
||||
|
||||
.body {
|
||||
padding: var(--spacing--sm);
|
||||
padding: var(--spacing--sm) var(--spacing--sm) 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing--3xs);
|
||||
}
|
||||
|
||||
.message {
|
||||
font-size: var(--font-size--2xs);
|
||||
color: var(--color--text);
|
||||
font-weight: var(--font-weight--medium);
|
||||
}
|
||||
|
||||
.preview {
|
||||
font-family: monospace;
|
||||
font-size: var(--font-size--3xs);
|
||||
color: var(--color--text--tint-1);
|
||||
word-break: break-all;
|
||||
padding: var(--spacing--2xs);
|
||||
background: var(--color--background);
|
||||
border-radius: var(--radius);
|
||||
border: var(--border);
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: var(--spacing--2xs);
|
||||
justify-content: flex-end;
|
||||
border-top: var(--border);
|
||||
padding: var(--spacing--xs) var(--spacing--sm);
|
||||
}
|
||||
|
||||
.splitButton {
|
||||
display: flex;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.splitButtonMain {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
.splitButtonDropdown {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.splitButtonCaret {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
border-left: 1px solid var(--color--foreground--tint-2);
|
||||
}
|
||||
</style>
|
||||
|
||||
+11
-38
@@ -7,12 +7,14 @@ import { computed, ref } from 'vue';
|
||||
import { useTelemetry } from '@/app/composables/useTelemetry';
|
||||
import { useInstanceAiStore, type PendingConfirmationItem } from '../instanceAi.store';
|
||||
import { useToolLabel } from '../toolLabels';
|
||||
import ConfirmationFooter from './ConfirmationFooter.vue';
|
||||
import DomainAccessApproval from './DomainAccessApproval.vue';
|
||||
import GatewayResourceDecision from './GatewayResourceDecision.vue';
|
||||
import InstanceAiCredentialSetup from './InstanceAiCredentialSetup.vue';
|
||||
import type { QuestionAnswer } from './InstanceAiQuestions.vue';
|
||||
import InstanceAiQuestions from './InstanceAiQuestions.vue';
|
||||
import InstanceAiWorkflowSetup from './InstanceAiWorkflowSetup.vue';
|
||||
import ConfirmationPreview from './ConfirmationPreview.vue';
|
||||
import PlanReviewPanel, { type PlannedTaskArg } from './PlanReviewPanel.vue';
|
||||
|
||||
const store = useInstanceAiStore();
|
||||
@@ -423,6 +425,7 @@ function isAllGenericApproval(items: PendingConfirmationItem[]): boolean {
|
||||
<N8nButton
|
||||
data-test-id="instance-ai-panel-confirm-approve-all"
|
||||
size="small"
|
||||
variant="subtle"
|
||||
@click="handleApproveAll(chunk.items)"
|
||||
>
|
||||
{{ i18n.baseText('instanceAi.confirmation.approveAll') }}
|
||||
@@ -450,13 +453,15 @@ function isAllGenericApproval(items: PendingConfirmationItem[]): boolean {
|
||||
<div v-else>
|
||||
<div :class="$style.approvalRow">
|
||||
<div :class="$style.approvalRowBody">
|
||||
<span :class="$style.toolLabel">
|
||||
<N8nText size="medium" bold>
|
||||
{{ getToolLabel(item.toolCall.toolName) }}
|
||||
</span>
|
||||
<span :class="$style.preview">{{ item.toolCall.confirmation.message }}</span>
|
||||
</N8nText>
|
||||
<ConfirmationPreview>{{
|
||||
item.toolCall.confirmation!.message
|
||||
}}</ConfirmationPreview>
|
||||
</div>
|
||||
|
||||
<div :class="$style.approvalActions">
|
||||
<ConfirmationFooter>
|
||||
<N8nButton
|
||||
data-test-id="instance-ai-panel-confirm-deny"
|
||||
size="small"
|
||||
@@ -477,7 +482,7 @@ function isAllGenericApproval(items: PendingConfirmationItem[]): boolean {
|
||||
>
|
||||
{{ i18n.baseText('instanceAi.confirmation.approve') }}
|
||||
</N8nButton>
|
||||
</div>
|
||||
</ConfirmationFooter>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -516,47 +521,15 @@ function isAllGenericApproval(items: PendingConfirmationItem[]): boolean {
|
||||
.approvalRow {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing--2xs);
|
||||
padding: var(--spacing--4xs) 0;
|
||||
font-size: var(--font-size--2xs);
|
||||
}
|
||||
|
||||
.toolLabel {
|
||||
display: inline-flex;
|
||||
white-space: nowrap;
|
||||
align-items: flex-start;
|
||||
gap: var(--spacing--3xs);
|
||||
font-size: var(--font-size--2xs);
|
||||
color: var(--color--text);
|
||||
margin-bottom: var(--spacing--xs);
|
||||
font-weight: var(--font-weight--medium);
|
||||
}
|
||||
|
||||
.approvalRowBody {
|
||||
padding: var(--spacing--sm) var(--spacing--sm);
|
||||
padding: var(--spacing--sm) var(--spacing--sm) 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing--3xs);
|
||||
}
|
||||
|
||||
.approvalActions {
|
||||
display: flex;
|
||||
gap: var(--spacing--2xs);
|
||||
justify-content: flex-end;
|
||||
border-top: var(--border);
|
||||
padding: var(--spacing--xs) var(--spacing--sm);
|
||||
}
|
||||
|
||||
.preview {
|
||||
font-family: monospace;
|
||||
font-size: var(--font-size--3xs);
|
||||
color: var(--color--text--tint-1);
|
||||
word-break: break-all;
|
||||
margin-bottom: var(--spacing--xs);
|
||||
padding: var(--spacing--2xs);
|
||||
background: var(--color--background);
|
||||
border-radius: var(--radius);
|
||||
border: var(--border);
|
||||
}
|
||||
|
||||
.textInputRow {
|
||||
|
||||
+3
-10
@@ -13,6 +13,7 @@ import { useRootStore } from '@n8n/stores/useRootStore';
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
||||
import { useTelemetry } from '@/app/composables/useTelemetry';
|
||||
import { useInstanceAiStore } from '../instanceAi.store';
|
||||
import ConfirmationFooter from './ConfirmationFooter.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
requestId: string;
|
||||
@@ -356,7 +357,7 @@ async function handleLater() {
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer :class="$style.footer">
|
||||
<ConfirmationFooter layout="row-between">
|
||||
<div :class="$style.footerNav">
|
||||
<N8nButton
|
||||
v-if="showArrows"
|
||||
@@ -411,7 +412,7 @@ async function handleLater() {
|
||||
@click="handleContinue"
|
||||
/>
|
||||
</div>
|
||||
</footer>
|
||||
</ConfirmationFooter>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -483,14 +484,6 @@ async function handleLater() {
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing--xs);
|
||||
border-top: var(--border);
|
||||
padding: var(--spacing--xs) var(--spacing--sm);
|
||||
}
|
||||
|
||||
.footerNav {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
|
||||
+4
-11
@@ -11,6 +11,7 @@
|
||||
import { ref, computed, watch, nextTick } from 'vue';
|
||||
import { N8nButton, N8nCheckbox, N8nIcon, N8nInput, N8nText } from '@n8n/design-system';
|
||||
import { useI18n } from '@n8n/i18n';
|
||||
import ConfirmationFooter from './ConfirmationFooter.vue';
|
||||
|
||||
const OTHER_SENTINEL = '__other__';
|
||||
|
||||
@@ -509,7 +510,7 @@ function onOptionMouseEnter(idx: number) {
|
||||
</Transition>
|
||||
|
||||
<!-- Footer -->
|
||||
<div :class="$style.footer">
|
||||
<ConfirmationFooter layout="row-between" bordered>
|
||||
<div :class="$style.pagination">
|
||||
<N8nButton
|
||||
variant="ghost"
|
||||
@@ -554,7 +555,7 @@ function onOptionMouseEnter(idx: number) {
|
||||
|
||||
<N8nButton
|
||||
v-if="showNextButton"
|
||||
:type="isNextEnabled ? 'primary' : 'secondary'"
|
||||
:variant="isNextEnabled ? 'solid' : 'outline'"
|
||||
size="small"
|
||||
:disabled="disabled || isSubmitted || !isNextEnabled"
|
||||
data-test-id="instance-ai-questions-next"
|
||||
@@ -563,7 +564,7 @@ function onOptionMouseEnter(idx: number) {
|
||||
{{ nextButtonLabel }}
|
||||
</N8nButton>
|
||||
</div>
|
||||
</div>
|
||||
</ConfirmationFooter>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -758,14 +759,6 @@ function onOptionMouseEnter(idx: number) {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-top: var(--border);
|
||||
padding: var(--spacing--xs) var(--spacing--sm);
|
||||
}
|
||||
|
||||
.pagination {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
+5
-12
@@ -31,6 +31,7 @@ import { useRootStore } from '@n8n/stores/useRootStore';
|
||||
import { NodeHelpers, isResourceLocatorValue, type INodeProperties } from 'n8n-workflow';
|
||||
import { computed, onMounted, onUnmounted, provide, ref, watch } from 'vue';
|
||||
import { useInstanceAiStore } from '../instanceAi.store';
|
||||
import ConfirmationFooter from './ConfirmationFooter.vue';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Types
|
||||
@@ -1038,7 +1039,7 @@ async function handleLater() {
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<footer :class="$style.footer">
|
||||
<ConfirmationFooter layout="row-between">
|
||||
<div :class="$style.footerNav">
|
||||
<N8nLink
|
||||
data-test-id="instance-ai-workflow-setup-review-details"
|
||||
@@ -1067,7 +1068,7 @@ async function handleLater() {
|
||||
@click="handleApply"
|
||||
/>
|
||||
</div>
|
||||
</footer>
|
||||
</ConfirmationFooter>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="currentCard"
|
||||
@@ -1214,7 +1215,7 @@ async function handleLater() {
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer :class="$style.footer">
|
||||
<ConfirmationFooter layout="row-between">
|
||||
<div :class="$style.footerNav">
|
||||
<N8nButton
|
||||
v-if="showArrows"
|
||||
@@ -1274,7 +1275,7 @@ async function handleLater() {
|
||||
@click="handleApply"
|
||||
/>
|
||||
</div>
|
||||
</footer>
|
||||
</ConfirmationFooter>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1399,14 +1400,6 @@ async function handleLater() {
|
||||
margin: 0 var(--spacing--sm);
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing--xs);
|
||||
border-top: var(--border);
|
||||
padding: var(--spacing--xs) var(--spacing--sm);
|
||||
}
|
||||
|
||||
.footerNav {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
|
||||
+4
-11
@@ -8,6 +8,7 @@
|
||||
import { N8nBadge, N8nButton, N8nIcon, type IconName } from '@n8n/design-system';
|
||||
import { useI18n } from '@n8n/i18n';
|
||||
import { computed, ref } from 'vue';
|
||||
import ConfirmationFooter from './ConfirmationFooter.vue';
|
||||
|
||||
export interface PlannedTaskArg {
|
||||
id: string;
|
||||
@@ -146,7 +147,7 @@ function handleRequestChanges() {
|
||||
</div>
|
||||
|
||||
<!-- Approval footer (hidden during loading and after resolution) -->
|
||||
<div v-if="!isResolved && !props.readOnly && !props.loading" :class="$style.footer">
|
||||
<ConfirmationFooter v-if="!isResolved && !props.readOnly && !props.loading" layout="column">
|
||||
<textarea
|
||||
v-model="feedback"
|
||||
:class="$style.feedbackTextarea"
|
||||
@@ -165,7 +166,7 @@ function handleRequestChanges() {
|
||||
{{ i18n.baseText('instanceAi.planReview.requestChanges') }}
|
||||
</N8nButton>
|
||||
<N8nButton
|
||||
type="primary"
|
||||
variant="solid"
|
||||
size="small"
|
||||
:disabled="disabled"
|
||||
data-test-id="instance-ai-plan-approve"
|
||||
@@ -174,7 +175,7 @@ function handleRequestChanges() {
|
||||
{{ i18n.baseText('instanceAi.planReview.approve') }}
|
||||
</N8nButton>
|
||||
</div>
|
||||
</div>
|
||||
</ConfirmationFooter>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -338,14 +339,6 @@ function handleRequestChanges() {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.footer {
|
||||
border-top: var(--border);
|
||||
padding: var(--spacing--xs) var(--spacing--sm);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing--2xs);
|
||||
}
|
||||
|
||||
.feedbackTextarea {
|
||||
width: 100%;
|
||||
padding: var(--spacing--2xs);
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
<script lang="ts" setup>
|
||||
import { N8nActionDropdown, N8nButton, N8nIconButton } from '@n8n/design-system';
|
||||
import type { ActionDropdownItem } from '@n8n/design-system/types';
|
||||
import { computed } from 'vue';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
variant?: 'solid' | 'outline' | 'destructive';
|
||||
size?: 'small' | 'medium';
|
||||
label: string;
|
||||
items: Array<ActionDropdownItem<string>>;
|
||||
disabled?: boolean;
|
||||
dataTestId?: string;
|
||||
dropdownTestId?: string;
|
||||
caretAriaLabel?: string;
|
||||
}>(),
|
||||
{
|
||||
variant: 'solid',
|
||||
size: 'small',
|
||||
caretAriaLabel: 'More options',
|
||||
},
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
click: [];
|
||||
select: [id: string];
|
||||
}>();
|
||||
|
||||
const hasSplit = computed(() => props.items.length > 0);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="hasSplit" :class="[$style.splitButton, variant === 'outline' && $style.outline]">
|
||||
<N8nButton
|
||||
:variant="variant"
|
||||
:class="$style.splitButtonMain"
|
||||
:label="label"
|
||||
:data-test-id="dataTestId"
|
||||
:size="size"
|
||||
:disabled="disabled"
|
||||
@click="emit('click')"
|
||||
/>
|
||||
<N8nActionDropdown
|
||||
:items="items"
|
||||
:class="$style.splitButtonDropdown"
|
||||
:data-test-id="dropdownTestId"
|
||||
placement="bottom-start"
|
||||
@select="(id: string) => emit('select', id)"
|
||||
>
|
||||
<template #activator>
|
||||
<N8nIconButton
|
||||
:variant="variant"
|
||||
icon="chevron-down"
|
||||
:class="$style.splitButtonCaret"
|
||||
:aria-label="caretAriaLabel"
|
||||
:size="size"
|
||||
:disabled="disabled"
|
||||
/>
|
||||
</template>
|
||||
</N8nActionDropdown>
|
||||
</div>
|
||||
<N8nButton
|
||||
v-else
|
||||
:variant="variant"
|
||||
:label="label"
|
||||
:data-test-id="dataTestId"
|
||||
:size="size"
|
||||
:disabled="disabled"
|
||||
@click="emit('click')"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style lang="scss" module>
|
||||
.splitButton {
|
||||
display: flex;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.splitButtonMain {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
.splitButtonDropdown {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.splitButtonCaret {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
border-left: 1px solid rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
// Outline: put the border on the wrapper, strip it from individual
|
||||
// buttons, and use a pseudo-element on the dropdown as the divider.
|
||||
.outline {
|
||||
border: 1px solid light-dark(var(--color--black-alpha-200), var(--color--white-alpha-100));
|
||||
border-radius: var(--radius);
|
||||
|
||||
.splitButtonMain,
|
||||
.splitButtonCaret {
|
||||
--button--border--shadow: 0 0 0 0 transparent;
|
||||
--button--border--shadow--hover: 0 0 0 0 transparent;
|
||||
--button--border--shadow--active: 0 0 0 0 transparent;
|
||||
border-left: 0;
|
||||
}
|
||||
|
||||
.splitButtonDropdown {
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 1px;
|
||||
background: light-dark(var(--color--black-alpha-200), var(--color--white-alpha-100));
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user