mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-24 20:24:05 -08:00
refactor(editor): Move a few components to script setup (no-changelog) (#10029)
This commit is contained in:
parent
293147642f
commit
2e9ab66602
|
@ -1,8 +1,379 @@
|
|||
<script lang="ts" setup>
|
||||
import { useStorage } from '@/composables/useStorage';
|
||||
|
||||
import type { INodeTypeDescription } from 'n8n-workflow';
|
||||
import PanelDragButton from './PanelDragButton.vue';
|
||||
|
||||
import { LOCAL_STORAGE_MAIN_PANEL_RELATIVE_WIDTH, MAIN_NODE_PANEL_WIDTH } from '@/constants';
|
||||
import { useNDVStore } from '@/stores/ndv.store';
|
||||
import { ndvEventBus } from '@/event-bus';
|
||||
import NDVFloatingNodes from '@/components/NDVFloatingNodes.vue';
|
||||
import { useDebounce } from '@/composables/useDebounce';
|
||||
import type { XYPosition } from '@/Interface';
|
||||
import { ref, onMounted, onBeforeUnmount, computed, watch } from 'vue';
|
||||
|
||||
const SIDE_MARGIN = 24;
|
||||
const SIDE_PANELS_MARGIN = 80;
|
||||
const MIN_PANEL_WIDTH = 280;
|
||||
const PANEL_WIDTH = 320;
|
||||
const PANEL_WIDTH_LARGE = 420;
|
||||
const MIN_WINDOW_WIDTH = 2 * (SIDE_MARGIN + SIDE_PANELS_MARGIN) + MIN_PANEL_WIDTH;
|
||||
|
||||
const initialMainPanelWidth: { [key: string]: number } = {
|
||||
regular: MAIN_NODE_PANEL_WIDTH,
|
||||
dragless: MAIN_NODE_PANEL_WIDTH,
|
||||
unknown: MAIN_NODE_PANEL_WIDTH,
|
||||
inputless: MAIN_NODE_PANEL_WIDTH,
|
||||
wide: MAIN_NODE_PANEL_WIDTH * 2,
|
||||
};
|
||||
|
||||
interface Props {
|
||||
isDraggable: boolean;
|
||||
hideInputAndOutput: boolean;
|
||||
nodeType: INodeTypeDescription | null;
|
||||
}
|
||||
|
||||
const { callDebounced } = useDebounce();
|
||||
const ndvStore = useNDVStore();
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const windowWidth = ref<number>(1);
|
||||
const isDragging = ref<boolean>(false);
|
||||
const initialized = ref<boolean>(false);
|
||||
|
||||
const emit = defineEmits<{
|
||||
init: [{ position: number }];
|
||||
dragstart: [{ position: number }];
|
||||
dragend: [{ position: number; windowWidth: number }];
|
||||
switchSelectedNode: [string];
|
||||
close: [];
|
||||
}>();
|
||||
|
||||
const slots = defineSlots<{
|
||||
input: unknown;
|
||||
output: unknown;
|
||||
main: unknown;
|
||||
}>();
|
||||
|
||||
onMounted(() => {
|
||||
setTotalWidth();
|
||||
|
||||
/*
|
||||
Only set(or restore) initial position if `mainPanelDimensions`
|
||||
is at the default state({relativeLeft:1, relativeRight: 1, relativeWidth: 1}) to make sure we use store values if they are set
|
||||
*/
|
||||
if (
|
||||
mainPanelDimensions.value.relativeLeft === 1 &&
|
||||
mainPanelDimensions.value.relativeRight === 1
|
||||
) {
|
||||
setMainPanelWidth();
|
||||
setPositions(getInitialLeftPosition(mainPanelDimensions.value.relativeWidth));
|
||||
restorePositionData();
|
||||
}
|
||||
|
||||
window.addEventListener('resize', setTotalWidth);
|
||||
emit('init', { position: mainPanelDimensions.value.relativeLeft });
|
||||
setTimeout(() => {
|
||||
initialized.value = true;
|
||||
}, 0);
|
||||
|
||||
ndvEventBus.on('setPositionByName', setPositionByName);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('resize', setTotalWidth);
|
||||
ndvEventBus.off('setPositionByName', setPositionByName);
|
||||
});
|
||||
|
||||
watch(windowWidth, (width) => {
|
||||
const minRelativeWidth = pxToRelativeWidth(MIN_PANEL_WIDTH);
|
||||
const isBelowMinWidthMainPanel = mainPanelDimensions.value.relativeWidth < minRelativeWidth;
|
||||
|
||||
// Prevent the panel resizing below MIN_PANEL_WIDTH whhile maintaing position
|
||||
if (isBelowMinWidthMainPanel) {
|
||||
setMainPanelWidth(minRelativeWidth);
|
||||
}
|
||||
|
||||
const isBelowMinLeft = minimumLeftPosition.value > mainPanelDimensions.value.relativeLeft;
|
||||
const isMaxRight = maximumRightPosition.value > mainPanelDimensions.value.relativeRight;
|
||||
|
||||
// When user is resizing from non-supported view(sub ~488px) we need to refit the panels
|
||||
if (width > MIN_WINDOW_WIDTH && isBelowMinLeft && isMaxRight) {
|
||||
setMainPanelWidth(minRelativeWidth);
|
||||
setPositions(getInitialLeftPosition(mainPanelDimensions.value.relativeWidth));
|
||||
}
|
||||
|
||||
setPositions(mainPanelDimensions.value.relativeLeft);
|
||||
});
|
||||
|
||||
const currentNodePaneType = computed((): string => {
|
||||
if (!hasInputSlot.value) return 'inputless';
|
||||
if (!props.isDraggable) return 'dragless';
|
||||
if (props.nodeType === null) return 'unknown';
|
||||
return props.nodeType.parameterPane ?? 'regular';
|
||||
});
|
||||
|
||||
const mainPanelDimensions = computed(
|
||||
(): {
|
||||
relativeWidth: number;
|
||||
relativeLeft: number;
|
||||
relativeRight: number;
|
||||
} => {
|
||||
return ndvStore.getMainPanelDimensions(currentNodePaneType.value);
|
||||
},
|
||||
);
|
||||
|
||||
const calculatedPositions = computed(
|
||||
(): { inputPanelRelativeRight: number; outputPanelRelativeLeft: number } => {
|
||||
const hasInput = slots.input !== undefined;
|
||||
const outputPanelRelativeLeft =
|
||||
mainPanelDimensions.value.relativeLeft + mainPanelDimensions.value.relativeWidth;
|
||||
|
||||
const inputPanelRelativeRight = hasInput
|
||||
? 1 - outputPanelRelativeLeft + mainPanelDimensions.value.relativeWidth
|
||||
: 1 - pxToRelativeWidth(SIDE_MARGIN);
|
||||
|
||||
return {
|
||||
inputPanelRelativeRight,
|
||||
outputPanelRelativeLeft,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
const outputPanelRelativeTranslate = computed((): number => {
|
||||
const panelMinLeft = 1 - pxToRelativeWidth(MIN_PANEL_WIDTH + SIDE_MARGIN);
|
||||
const currentRelativeLeftDelta = calculatedPositions.value.outputPanelRelativeLeft - panelMinLeft;
|
||||
return currentRelativeLeftDelta > 0 ? currentRelativeLeftDelta : 0;
|
||||
});
|
||||
|
||||
const supportedResizeDirections = computed((): string[] => {
|
||||
const supportedDirections = ['right'];
|
||||
|
||||
if (props.isDraggable) supportedDirections.push('left');
|
||||
return supportedDirections;
|
||||
});
|
||||
|
||||
const hasInputSlot = computed((): boolean => {
|
||||
return slots.input !== undefined;
|
||||
});
|
||||
|
||||
const inputPanelMargin = computed(() => pxToRelativeWidth(SIDE_PANELS_MARGIN));
|
||||
|
||||
const minimumLeftPosition = computed((): number => {
|
||||
if (windowWidth.value < MIN_WINDOW_WIDTH) return pxToRelativeWidth(1);
|
||||
|
||||
if (!hasInputSlot.value) return pxToRelativeWidth(SIDE_MARGIN);
|
||||
return pxToRelativeWidth(SIDE_MARGIN + 20) + inputPanelMargin.value;
|
||||
});
|
||||
|
||||
const maximumRightPosition = computed((): number => {
|
||||
if (windowWidth.value < MIN_WINDOW_WIDTH) return pxToRelativeWidth(1);
|
||||
|
||||
return pxToRelativeWidth(SIDE_MARGIN + 20) + inputPanelMargin.value;
|
||||
});
|
||||
|
||||
const canMoveLeft = computed((): boolean => {
|
||||
return mainPanelDimensions.value.relativeLeft > minimumLeftPosition.value;
|
||||
});
|
||||
|
||||
const canMoveRight = computed((): boolean => {
|
||||
return mainPanelDimensions.value.relativeRight > maximumRightPosition.value;
|
||||
});
|
||||
|
||||
const mainPanelStyles = computed((): { left: string; right: string } => {
|
||||
return {
|
||||
left: `${relativeWidthToPx(mainPanelDimensions.value.relativeLeft)}px`,
|
||||
right: `${relativeWidthToPx(mainPanelDimensions.value.relativeRight)}px`,
|
||||
};
|
||||
});
|
||||
|
||||
const inputPanelStyles = computed((): { right: string } => {
|
||||
return {
|
||||
right: `${relativeWidthToPx(calculatedPositions.value.inputPanelRelativeRight)}px`,
|
||||
};
|
||||
});
|
||||
|
||||
const outputPanelStyles = computed((): { left: string; transform: string } => {
|
||||
return {
|
||||
left: `${relativeWidthToPx(calculatedPositions.value.outputPanelRelativeLeft)}px`,
|
||||
transform: `translateX(-${relativeWidthToPx(outputPanelRelativeTranslate.value)}px)`,
|
||||
};
|
||||
});
|
||||
|
||||
const hasDoubleWidth = computed((): boolean => {
|
||||
return props.nodeType?.parameterPane === 'wide';
|
||||
});
|
||||
|
||||
const fixedPanelWidth = computed((): number => {
|
||||
const multiplier = hasDoubleWidth.value ? 2 : 1;
|
||||
|
||||
if (windowWidth.value > 1700) {
|
||||
return PANEL_WIDTH_LARGE * multiplier;
|
||||
}
|
||||
|
||||
return PANEL_WIDTH * multiplier;
|
||||
});
|
||||
|
||||
const onSwitchSelectedNode = (node: string) => emit('switchSelectedNode', node);
|
||||
|
||||
function getInitialLeftPosition(width: number): number {
|
||||
if (currentNodePaneType.value === 'dragless')
|
||||
return pxToRelativeWidth(SIDE_MARGIN + 1 + fixedPanelWidth.value);
|
||||
|
||||
return hasInputSlot.value ? 0.5 - width / 2 : minimumLeftPosition.value;
|
||||
}
|
||||
|
||||
function setMainPanelWidth(relativeWidth?: number): void {
|
||||
const mainPanelRelativeWidth =
|
||||
relativeWidth || pxToRelativeWidth(initialMainPanelWidth[currentNodePaneType.value]);
|
||||
|
||||
ndvStore.setMainPanelDimensions({
|
||||
panelType: currentNodePaneType.value,
|
||||
dimensions: {
|
||||
relativeWidth: mainPanelRelativeWidth,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function setPositions(relativeLeft: number): void {
|
||||
const mainPanelRelativeLeft =
|
||||
relativeLeft || 1 - calculatedPositions.value.inputPanelRelativeRight;
|
||||
const mainPanelRelativeRight =
|
||||
1 - mainPanelRelativeLeft - mainPanelDimensions.value.relativeWidth;
|
||||
|
||||
const isMaxRight = maximumRightPosition.value > mainPanelRelativeRight;
|
||||
const isMinLeft = minimumLeftPosition.value > mainPanelRelativeLeft;
|
||||
const isInputless = currentNodePaneType.value === 'inputless';
|
||||
|
||||
if (isMinLeft) {
|
||||
ndvStore.setMainPanelDimensions({
|
||||
panelType: currentNodePaneType.value,
|
||||
dimensions: {
|
||||
relativeLeft: minimumLeftPosition.value,
|
||||
relativeRight: 1 - mainPanelDimensions.value.relativeWidth - minimumLeftPosition.value,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (isMaxRight) {
|
||||
ndvStore.setMainPanelDimensions({
|
||||
panelType: currentNodePaneType.value,
|
||||
dimensions: {
|
||||
relativeLeft: 1 - mainPanelDimensions.value.relativeWidth - maximumRightPosition.value,
|
||||
relativeRight: maximumRightPosition.value,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
ndvStore.setMainPanelDimensions({
|
||||
panelType: currentNodePaneType.value,
|
||||
dimensions: {
|
||||
relativeLeft: isInputless ? minimumLeftPosition.value : mainPanelRelativeLeft,
|
||||
relativeRight: mainPanelRelativeRight,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function setPositionByName(position: 'minLeft' | 'maxRight' | 'initial') {
|
||||
const positionByName: Record<string, number> = {
|
||||
minLeft: minimumLeftPosition.value,
|
||||
maxRight: maximumRightPosition.value,
|
||||
initial: getInitialLeftPosition(mainPanelDimensions.value.relativeWidth),
|
||||
};
|
||||
|
||||
setPositions(positionByName[position]);
|
||||
}
|
||||
|
||||
function pxToRelativeWidth(px: number): number {
|
||||
return px / windowWidth.value;
|
||||
}
|
||||
|
||||
function relativeWidthToPx(relativeWidth: number) {
|
||||
return relativeWidth * windowWidth.value;
|
||||
}
|
||||
|
||||
function onResizeStart() {
|
||||
setTotalWidth();
|
||||
}
|
||||
|
||||
function onResizeEnd() {
|
||||
storePositionData();
|
||||
}
|
||||
|
||||
function onResizeDebounced(data: { direction: string; x: number; width: number }) {
|
||||
if (initialized.value) {
|
||||
void callDebounced(onResize, { debounceTime: 10, trailing: true }, data);
|
||||
}
|
||||
}
|
||||
|
||||
function onResize({ direction, x, width }: { direction: string; x: number; width: number }) {
|
||||
const relativeDistance = pxToRelativeWidth(x);
|
||||
const relativeWidth = pxToRelativeWidth(width);
|
||||
|
||||
if (direction === 'left' && relativeDistance <= minimumLeftPosition.value) return;
|
||||
if (direction === 'right' && 1 - relativeDistance <= maximumRightPosition.value) return;
|
||||
if (width <= MIN_PANEL_WIDTH) return;
|
||||
|
||||
setMainPanelWidth(relativeWidth);
|
||||
setPositions(direction === 'left' ? relativeDistance : mainPanelDimensions.value.relativeLeft);
|
||||
}
|
||||
|
||||
function restorePositionData() {
|
||||
const storedPanelWidthData = useStorage(
|
||||
`${LOCAL_STORAGE_MAIN_PANEL_RELATIVE_WIDTH}_${currentNodePaneType.value}`,
|
||||
).value;
|
||||
|
||||
if (storedPanelWidthData) {
|
||||
const parsedWidth = parseFloat(storedPanelWidthData);
|
||||
setMainPanelWidth(parsedWidth);
|
||||
const initialPosition = getInitialLeftPosition(parsedWidth);
|
||||
|
||||
setPositions(initialPosition);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function storePositionData() {
|
||||
useStorage(`${LOCAL_STORAGE_MAIN_PANEL_RELATIVE_WIDTH}_${currentNodePaneType.value}`).value =
|
||||
mainPanelDimensions.value.relativeWidth.toString();
|
||||
}
|
||||
|
||||
function onDragStart() {
|
||||
isDragging.value = true;
|
||||
emit('dragstart', { position: mainPanelDimensions.value.relativeLeft });
|
||||
}
|
||||
|
||||
function onDrag(position: XYPosition) {
|
||||
const relativeLeft = pxToRelativeWidth(position[0]) - mainPanelDimensions.value.relativeWidth / 2;
|
||||
|
||||
setPositions(relativeLeft);
|
||||
}
|
||||
|
||||
function onDragEnd() {
|
||||
setTimeout(() => {
|
||||
isDragging.value = false;
|
||||
emit('dragend', {
|
||||
windowWidth: windowWidth.value,
|
||||
position: mainPanelDimensions.value.relativeLeft,
|
||||
});
|
||||
}, 0);
|
||||
storePositionData();
|
||||
}
|
||||
|
||||
function setTotalWidth() {
|
||||
windowWidth.value = window.innerWidth;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<NDVFloatingNodes
|
||||
v-if="activeNode"
|
||||
:root-node="activeNode"
|
||||
v-if="ndvStore.activeNode"
|
||||
:root-node="ndvStore.activeNode"
|
||||
@switch-selected-node="onSwitchSelectedNode"
|
||||
/>
|
||||
<div v-if="!hideInputAndOutput" :class="$style.inputPanel" :style="inputPanelStyles">
|
||||
|
@ -41,377 +412,6 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import type { PropType } from 'vue';
|
||||
import { mapStores } from 'pinia';
|
||||
import { get } from 'lodash-es';
|
||||
import { useStorage } from '@/composables/useStorage';
|
||||
|
||||
import type { INodeTypeDescription } from 'n8n-workflow';
|
||||
import PanelDragButton from './PanelDragButton.vue';
|
||||
|
||||
import { LOCAL_STORAGE_MAIN_PANEL_RELATIVE_WIDTH, MAIN_NODE_PANEL_WIDTH } from '@/constants';
|
||||
import { useNDVStore } from '@/stores/ndv.store';
|
||||
import { ndvEventBus } from '@/event-bus';
|
||||
import NDVFloatingNodes from '@/components/NDVFloatingNodes.vue';
|
||||
import { useDebounce } from '@/composables/useDebounce';
|
||||
import type { XYPosition } from '@/Interface';
|
||||
|
||||
const SIDE_MARGIN = 24;
|
||||
const SIDE_PANELS_MARGIN = 80;
|
||||
const MIN_PANEL_WIDTH = 280;
|
||||
const PANEL_WIDTH = 320;
|
||||
const PANEL_WIDTH_LARGE = 420;
|
||||
|
||||
const initialMainPanelWidth: { [key: string]: number } = {
|
||||
regular: MAIN_NODE_PANEL_WIDTH,
|
||||
dragless: MAIN_NODE_PANEL_WIDTH,
|
||||
unknown: MAIN_NODE_PANEL_WIDTH,
|
||||
inputless: MAIN_NODE_PANEL_WIDTH,
|
||||
wide: MAIN_NODE_PANEL_WIDTH * 2,
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
name: 'NDVDraggablePanels',
|
||||
components: {
|
||||
PanelDragButton,
|
||||
NDVFloatingNodes,
|
||||
},
|
||||
props: {
|
||||
isDraggable: {
|
||||
type: Boolean,
|
||||
},
|
||||
hideInputAndOutput: {
|
||||
type: Boolean,
|
||||
},
|
||||
position: {
|
||||
type: Number,
|
||||
},
|
||||
nodeType: {
|
||||
type: Object as PropType<INodeTypeDescription | null>,
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const { callDebounced } = useDebounce();
|
||||
|
||||
return { callDebounced };
|
||||
},
|
||||
data(): {
|
||||
windowWidth: number;
|
||||
isDragging: boolean;
|
||||
MIN_PANEL_WIDTH: number;
|
||||
initialized: boolean;
|
||||
} {
|
||||
return {
|
||||
windowWidth: 1,
|
||||
isDragging: false,
|
||||
MIN_PANEL_WIDTH,
|
||||
initialized: false,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.setTotalWidth();
|
||||
|
||||
/*
|
||||
Only set(or restore) initial position if `mainPanelDimensions`
|
||||
is at the default state({relativeLeft:1, relativeRight: 1, relativeWidth: 1}) to make sure we use store values if they are set
|
||||
*/
|
||||
if (
|
||||
this.mainPanelDimensions.relativeLeft === 1 &&
|
||||
this.mainPanelDimensions.relativeRight === 1
|
||||
) {
|
||||
this.setMainPanelWidth();
|
||||
this.setPositions(this.getInitialLeftPosition(this.mainPanelDimensions.relativeWidth));
|
||||
this.restorePositionData();
|
||||
}
|
||||
|
||||
window.addEventListener('resize', this.setTotalWidth);
|
||||
this.$emit('init', { position: this.mainPanelDimensions.relativeLeft });
|
||||
setTimeout(() => {
|
||||
this.initialized = true;
|
||||
}, 0);
|
||||
|
||||
ndvEventBus.on('setPositionByName', this.setPositionByName);
|
||||
},
|
||||
beforeUnmount() {
|
||||
window.removeEventListener('resize', this.setTotalWidth);
|
||||
ndvEventBus.off('setPositionByName', this.setPositionByName);
|
||||
},
|
||||
computed: {
|
||||
...mapStores(useNDVStore),
|
||||
mainPanelDimensions(): {
|
||||
relativeWidth: number;
|
||||
relativeLeft: number;
|
||||
relativeRight: number;
|
||||
} {
|
||||
return this.ndvStore.getMainPanelDimensions(this.currentNodePaneType);
|
||||
},
|
||||
activeNode() {
|
||||
return this.ndvStore.activeNode;
|
||||
},
|
||||
supportedResizeDirections(): string[] {
|
||||
const supportedDirections = ['right'];
|
||||
|
||||
if (this.isDraggable) supportedDirections.push('left');
|
||||
return supportedDirections;
|
||||
},
|
||||
currentNodePaneType(): string {
|
||||
if (!this.hasInputSlot) return 'inputless';
|
||||
if (!this.isDraggable) return 'dragless';
|
||||
if (this.nodeType === null) return 'unknown';
|
||||
return get(this, 'nodeType.parameterPane') || 'regular';
|
||||
},
|
||||
hasInputSlot(): boolean {
|
||||
return this.$slots.input !== undefined;
|
||||
},
|
||||
inputPanelMargin(): number {
|
||||
return this.pxToRelativeWidth(SIDE_PANELS_MARGIN);
|
||||
},
|
||||
minWindowWidth(): number {
|
||||
return 2 * (SIDE_MARGIN + SIDE_PANELS_MARGIN) + MIN_PANEL_WIDTH;
|
||||
},
|
||||
minimumLeftPosition(): number {
|
||||
if (this.windowWidth < this.minWindowWidth) return this.pxToRelativeWidth(1);
|
||||
|
||||
if (!this.hasInputSlot) return this.pxToRelativeWidth(SIDE_MARGIN);
|
||||
return this.pxToRelativeWidth(SIDE_MARGIN + 20) + this.inputPanelMargin;
|
||||
},
|
||||
maximumRightPosition(): number {
|
||||
if (this.windowWidth < this.minWindowWidth) return this.pxToRelativeWidth(1);
|
||||
|
||||
return this.pxToRelativeWidth(SIDE_MARGIN + 20) + this.inputPanelMargin;
|
||||
},
|
||||
canMoveLeft(): boolean {
|
||||
return this.mainPanelDimensions.relativeLeft > this.minimumLeftPosition;
|
||||
},
|
||||
canMoveRight(): boolean {
|
||||
return this.mainPanelDimensions.relativeRight > this.maximumRightPosition;
|
||||
},
|
||||
mainPanelStyles(): { left: string; right: string } {
|
||||
return {
|
||||
left: `${this.relativeWidthToPx(this.mainPanelDimensions.relativeLeft)}px`,
|
||||
right: `${this.relativeWidthToPx(this.mainPanelDimensions.relativeRight)}px`,
|
||||
};
|
||||
},
|
||||
inputPanelStyles(): { right: string } {
|
||||
return {
|
||||
right: `${this.relativeWidthToPx(this.calculatedPositions.inputPanelRelativeRight)}px`,
|
||||
};
|
||||
},
|
||||
outputPanelStyles(): { left: string; transform: string } {
|
||||
return {
|
||||
left: `${this.relativeWidthToPx(this.calculatedPositions.outputPanelRelativeLeft)}px`,
|
||||
transform: `translateX(-${this.relativeWidthToPx(this.outputPanelRelativeTranslate)}px)`,
|
||||
};
|
||||
},
|
||||
calculatedPositions(): { inputPanelRelativeRight: number; outputPanelRelativeLeft: number } {
|
||||
const hasInput = this.$slots.input !== undefined;
|
||||
const outputPanelRelativeLeft =
|
||||
this.mainPanelDimensions.relativeLeft + this.mainPanelDimensions.relativeWidth;
|
||||
|
||||
const inputPanelRelativeRight = hasInput
|
||||
? 1 - outputPanelRelativeLeft + this.mainPanelDimensions.relativeWidth
|
||||
: 1 - this.pxToRelativeWidth(SIDE_MARGIN);
|
||||
|
||||
return {
|
||||
inputPanelRelativeRight,
|
||||
outputPanelRelativeLeft,
|
||||
};
|
||||
},
|
||||
outputPanelRelativeTranslate(): number {
|
||||
const panelMinLeft = 1 - this.pxToRelativeWidth(MIN_PANEL_WIDTH + SIDE_MARGIN);
|
||||
const currentRelativeLeftDelta =
|
||||
this.calculatedPositions.outputPanelRelativeLeft - panelMinLeft;
|
||||
return currentRelativeLeftDelta > 0 ? currentRelativeLeftDelta : 0;
|
||||
},
|
||||
hasDoubleWidth(): boolean {
|
||||
return get(this, 'nodeType.parameterPane') === 'wide';
|
||||
},
|
||||
fixedPanelWidth(): number {
|
||||
const multiplier = this.hasDoubleWidth ? 2 : 1;
|
||||
|
||||
if (this.windowWidth > 1700) {
|
||||
return PANEL_WIDTH_LARGE * multiplier;
|
||||
}
|
||||
|
||||
return PANEL_WIDTH * multiplier;
|
||||
},
|
||||
isBelowMinWidthMainPanel(): boolean {
|
||||
const minRelativeWidth = this.pxToRelativeWidth(MIN_PANEL_WIDTH);
|
||||
return this.mainPanelDimensions.relativeWidth < minRelativeWidth;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
windowWidth(windowWidth) {
|
||||
const minRelativeWidth = this.pxToRelativeWidth(MIN_PANEL_WIDTH);
|
||||
// Prevent the panel resizing below MIN_PANEL_WIDTH whhile maintaing position
|
||||
if (this.isBelowMinWidthMainPanel) {
|
||||
this.setMainPanelWidth(minRelativeWidth);
|
||||
}
|
||||
|
||||
const isBelowMinLeft = this.minimumLeftPosition > this.mainPanelDimensions.relativeLeft;
|
||||
const isMaxRight = this.maximumRightPosition > this.mainPanelDimensions.relativeRight;
|
||||
|
||||
// When user is resizing from non-supported view(sub ~488px) we need to refit the panels
|
||||
if (windowWidth > this.minWindowWidth && isBelowMinLeft && isMaxRight) {
|
||||
this.setMainPanelWidth(minRelativeWidth);
|
||||
this.setPositions(this.getInitialLeftPosition(this.mainPanelDimensions.relativeWidth));
|
||||
}
|
||||
|
||||
this.setPositions(this.mainPanelDimensions.relativeLeft);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onSwitchSelectedNode(node: string) {
|
||||
this.$emit('switchSelectedNode', node);
|
||||
},
|
||||
getInitialLeftPosition(width: number) {
|
||||
if (this.currentNodePaneType === 'dragless')
|
||||
return this.pxToRelativeWidth(SIDE_MARGIN + 1 + this.fixedPanelWidth);
|
||||
|
||||
return this.hasInputSlot ? 0.5 - width / 2 : this.minimumLeftPosition;
|
||||
},
|
||||
setMainPanelWidth(relativeWidth?: number) {
|
||||
const mainPanelRelativeWidth =
|
||||
relativeWidth || this.pxToRelativeWidth(initialMainPanelWidth[this.currentNodePaneType]);
|
||||
|
||||
this.ndvStore.setMainPanelDimensions({
|
||||
panelType: this.currentNodePaneType,
|
||||
dimensions: {
|
||||
relativeWidth: mainPanelRelativeWidth,
|
||||
},
|
||||
});
|
||||
},
|
||||
setPositions(relativeLeft: number) {
|
||||
const mainPanelRelativeLeft =
|
||||
relativeLeft || 1 - this.calculatedPositions.inputPanelRelativeRight;
|
||||
const mainPanelRelativeRight =
|
||||
1 - mainPanelRelativeLeft - this.mainPanelDimensions.relativeWidth;
|
||||
|
||||
const isMaxRight = this.maximumRightPosition > mainPanelRelativeRight;
|
||||
const isMinLeft = this.minimumLeftPosition > mainPanelRelativeLeft;
|
||||
const isInputless = this.currentNodePaneType === 'inputless';
|
||||
|
||||
if (isMinLeft) {
|
||||
this.ndvStore.setMainPanelDimensions({
|
||||
panelType: this.currentNodePaneType,
|
||||
dimensions: {
|
||||
relativeLeft: this.minimumLeftPosition,
|
||||
relativeRight: 1 - this.mainPanelDimensions.relativeWidth - this.minimumLeftPosition,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (isMaxRight) {
|
||||
this.ndvStore.setMainPanelDimensions({
|
||||
panelType: this.currentNodePaneType,
|
||||
dimensions: {
|
||||
relativeLeft: 1 - this.mainPanelDimensions.relativeWidth - this.maximumRightPosition,
|
||||
relativeRight: this.maximumRightPosition,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.ndvStore.setMainPanelDimensions({
|
||||
panelType: this.currentNodePaneType,
|
||||
dimensions: {
|
||||
relativeLeft: isInputless ? this.minimumLeftPosition : mainPanelRelativeLeft,
|
||||
relativeRight: mainPanelRelativeRight,
|
||||
},
|
||||
});
|
||||
},
|
||||
setPositionByName(position: 'minLeft' | 'maxRight' | 'initial') {
|
||||
const positionByName: Record<string, number> = {
|
||||
minLeft: this.minimumLeftPosition,
|
||||
maxRight: this.maximumRightPosition,
|
||||
initial: this.getInitialLeftPosition(this.mainPanelDimensions.relativeWidth),
|
||||
};
|
||||
|
||||
this.setPositions(positionByName[position]);
|
||||
},
|
||||
pxToRelativeWidth(px: number) {
|
||||
return px / this.windowWidth;
|
||||
},
|
||||
relativeWidthToPx(relativeWidth: number) {
|
||||
return relativeWidth * this.windowWidth;
|
||||
},
|
||||
onResizeStart() {
|
||||
this.setTotalWidth();
|
||||
},
|
||||
onResizeEnd() {
|
||||
this.storePositionData();
|
||||
},
|
||||
onResizeDebounced(data: { direction: string; x: number; width: number }) {
|
||||
if (this.initialized) {
|
||||
void this.callDebounced(this.onResize, { debounceTime: 10, trailing: true }, data);
|
||||
}
|
||||
},
|
||||
onResize({ direction, x, width }: { direction: string; x: number; width: number }) {
|
||||
const relativeDistance = this.pxToRelativeWidth(x);
|
||||
const relativeWidth = this.pxToRelativeWidth(width);
|
||||
|
||||
if (direction === 'left' && relativeDistance <= this.minimumLeftPosition) return;
|
||||
if (direction === 'right' && 1 - relativeDistance <= this.maximumRightPosition) return;
|
||||
if (width <= MIN_PANEL_WIDTH) return;
|
||||
|
||||
this.setMainPanelWidth(relativeWidth);
|
||||
this.setPositions(
|
||||
direction === 'left' ? relativeDistance : this.mainPanelDimensions.relativeLeft,
|
||||
);
|
||||
},
|
||||
restorePositionData() {
|
||||
const storedPanelWidthData = useStorage(
|
||||
`${LOCAL_STORAGE_MAIN_PANEL_RELATIVE_WIDTH}_${this.currentNodePaneType}`,
|
||||
).value;
|
||||
|
||||
if (storedPanelWidthData) {
|
||||
const parsedWidth = parseFloat(storedPanelWidthData);
|
||||
this.setMainPanelWidth(parsedWidth);
|
||||
const initialPosition = this.getInitialLeftPosition(parsedWidth);
|
||||
|
||||
this.setPositions(initialPosition);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
storePositionData() {
|
||||
useStorage(`${LOCAL_STORAGE_MAIN_PANEL_RELATIVE_WIDTH}_${this.currentNodePaneType}`).value =
|
||||
this.mainPanelDimensions.relativeWidth.toString();
|
||||
},
|
||||
onDragStart() {
|
||||
this.isDragging = true;
|
||||
this.$emit('dragstart', { position: this.mainPanelDimensions.relativeLeft });
|
||||
},
|
||||
onDrag(position: XYPosition) {
|
||||
const relativeLeft =
|
||||
this.pxToRelativeWidth(position[0]) - this.mainPanelDimensions.relativeWidth / 2;
|
||||
|
||||
this.setPositions(relativeLeft);
|
||||
},
|
||||
onDragEnd() {
|
||||
setTimeout(() => {
|
||||
this.isDragging = false;
|
||||
this.$emit('dragend', {
|
||||
windowWidth: this.windowWidth,
|
||||
position: this.mainPanelDimensions.relativeLeft,
|
||||
});
|
||||
}, 0);
|
||||
this.storePositionData();
|
||||
},
|
||||
setTotalWidth() {
|
||||
this.windowWidth = window.innerWidth;
|
||||
},
|
||||
close() {
|
||||
this.$emit('close');
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.dataPanel {
|
||||
position: absolute;
|
||||
|
|
|
@ -1,3 +1,39 @@
|
|||
<script lang="ts" setup>
|
||||
import KeyboardShortcutTooltip from '@/components/KeyboardShortcutTooltip.vue';
|
||||
import { useI18n } from '@/composables/useI18n';
|
||||
import { computed } from 'vue';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
saved: boolean;
|
||||
isSaving: boolean;
|
||||
disabled: boolean;
|
||||
type: string;
|
||||
withShortcut: boolean;
|
||||
shortcutTooltip?: string;
|
||||
savingLabel?: string;
|
||||
}>(),
|
||||
{
|
||||
isSaving: false,
|
||||
type: 'primary',
|
||||
withShortcut: false,
|
||||
disabled: false,
|
||||
},
|
||||
);
|
||||
|
||||
const i18n = useI18n();
|
||||
|
||||
const saveButtonLabel = computed(() => {
|
||||
return props.isSaving
|
||||
? props.savingLabel ?? i18n.baseText('saveButton.saving')
|
||||
: i18n.baseText('saveButton.save');
|
||||
});
|
||||
|
||||
const shortcutTooltipLabel = computed(() => {
|
||||
return props.shortcutTooltip ?? i18n.baseText('saveButton.save');
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span :class="$style.container" data-test-id="save-button">
|
||||
<span v-if="saved" :class="$style.saved">{{ $locale.baseText('saveButton.saved') }}</span>
|
||||
|
@ -28,59 +64,6 @@
|
|||
</span>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import KeyboardShortcutTooltip from '@/components/KeyboardShortcutTooltip.vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'SaveButton',
|
||||
components: {
|
||||
KeyboardShortcutTooltip,
|
||||
},
|
||||
props: {
|
||||
saved: {
|
||||
type: Boolean,
|
||||
},
|
||||
isSaving: {
|
||||
type: Boolean,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
},
|
||||
saveLabel: {
|
||||
type: String,
|
||||
},
|
||||
savingLabel: {
|
||||
type: String,
|
||||
},
|
||||
savedLabel: {
|
||||
type: String,
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: 'primary',
|
||||
},
|
||||
withShortcut: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
shortcutTooltip: {
|
||||
type: String,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
saveButtonLabel() {
|
||||
return this.isSaving
|
||||
? this.$locale.baseText('saveButton.saving')
|
||||
: this.$locale.baseText('saveButton.save');
|
||||
},
|
||||
shortcutTooltipLabel() {
|
||||
return this.shortcutTooltip ?? this.$locale.baseText('saveButton.save');
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.container {
|
||||
display: inline-flex;
|
||||
|
|
|
@ -1,47 +1,50 @@
|
|||
<script lang="ts" setup>
|
||||
import { useCredentialsStore } from '@/stores/credentials.store';
|
||||
import { useI18n } from '@/composables/useI18n';
|
||||
import { computed } from 'vue';
|
||||
|
||||
const credentialsStore = useCredentialsStore();
|
||||
const i18n = useI18n();
|
||||
|
||||
const props = defineProps<{
|
||||
activeCredentialType: string;
|
||||
scopes: string[];
|
||||
}>();
|
||||
|
||||
const shortCredentialDisplayName = computed((): string => {
|
||||
const oauth1Api = i18n.baseText('generic.oauth1Api');
|
||||
const oauth2Api = i18n.baseText('generic.oauth2Api');
|
||||
|
||||
return (
|
||||
credentialsStore
|
||||
.getCredentialTypeByName(props.activeCredentialType)
|
||||
?.displayName.replace(new RegExp(`${oauth1Api}|${oauth2Api}`), '')
|
||||
.trim() || ''
|
||||
);
|
||||
});
|
||||
|
||||
const scopesShortContent = computed((): string => {
|
||||
return i18n.baseText('nodeSettings.scopes.notice', {
|
||||
adjustToNumber: props.scopes.length,
|
||||
interpolate: {
|
||||
activeCredential: shortCredentialDisplayName.value,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
const scopesFullContent = computed((): string => {
|
||||
return i18n.baseText('nodeSettings.scopes.expandedNoticeWithScopes', {
|
||||
adjustToNumber: props.scopes.length,
|
||||
interpolate: {
|
||||
activeCredential: shortCredentialDisplayName.value,
|
||||
scopes: props.scopes
|
||||
.map((s: string) => (s.includes('/') ? s.split('/').pop() : s))
|
||||
.join('<br>'),
|
||||
},
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n8n-notice :content="scopesShortContent" :full-content="scopesFullContent" />
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import { mapStores } from 'pinia';
|
||||
import { useCredentialsStore } from '@/stores/credentials.store';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ScopesNotice',
|
||||
props: ['activeCredentialType', 'scopes'],
|
||||
computed: {
|
||||
...mapStores(useCredentialsStore),
|
||||
scopesShortContent(): string {
|
||||
return this.$locale.baseText('nodeSettings.scopes.notice', {
|
||||
adjustToNumber: this.scopes.length,
|
||||
interpolate: {
|
||||
activeCredential: this.shortCredentialDisplayName,
|
||||
},
|
||||
});
|
||||
},
|
||||
scopesFullContent(): string {
|
||||
return this.$locale.baseText('nodeSettings.scopes.expandedNoticeWithScopes', {
|
||||
adjustToNumber: this.scopes.length,
|
||||
interpolate: {
|
||||
activeCredential: this.shortCredentialDisplayName,
|
||||
scopes: this.scopes
|
||||
.map((s: string) => (s.includes('/') ? s.split('/').pop() : s))
|
||||
.join('<br>'),
|
||||
},
|
||||
});
|
||||
},
|
||||
shortCredentialDisplayName(): string {
|
||||
const oauth1Api = this.$locale.baseText('generic.oauth1Api');
|
||||
const oauth2Api = this.$locale.baseText('generic.oauth2Api');
|
||||
|
||||
return (
|
||||
this.credentialsStore
|
||||
.getCredentialTypeByName(this.activeCredentialType)
|
||||
?.displayName.replace(new RegExp(`${oauth1Api}|${oauth2Api}`), '')
|
||||
.trim() || ''
|
||||
);
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -1,75 +1,74 @@
|
|||
<template>
|
||||
<span v-show="false" />
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import { mapStores } from 'pinia';
|
||||
<script lang="ts" setup>
|
||||
import { useRootStore } from '@/stores/root.store';
|
||||
import { useSettingsStore } from '@/stores/settings.store';
|
||||
import { useUsersStore } from '@/stores/users.store';
|
||||
import type { ITelemetrySettings } from 'n8n-workflow';
|
||||
import { useProjectsStore } from '@/stores/projects.store';
|
||||
import { computed, onMounted, watch, ref } from 'vue';
|
||||
import { useTelemetry } from '@/composables/useTelemetry';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Telemetry',
|
||||
data() {
|
||||
return {
|
||||
isTelemetryInitialized: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapStores(useRootStore, useSettingsStore, useUsersStore, useProjectsStore),
|
||||
currentUserId(): string {
|
||||
return this.usersStore.currentUserId ?? '';
|
||||
},
|
||||
isTelemetryEnabledOnRoute(): boolean {
|
||||
const routeMeta = this.$route.meta as { telemetry?: { disabled?: boolean } } | undefined;
|
||||
return routeMeta?.telemetry ? !routeMeta.telemetry.disabled : true;
|
||||
},
|
||||
telemetry(): ITelemetrySettings {
|
||||
return this.settingsStore.telemetry;
|
||||
},
|
||||
isTelemetryEnabled(): boolean {
|
||||
return !!this.telemetry?.enabled;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
telemetry() {
|
||||
this.init();
|
||||
},
|
||||
currentUserId(userId) {
|
||||
if (this.isTelemetryEnabled) {
|
||||
this.$telemetry.identify(this.rootStore.instanceId, userId);
|
||||
}
|
||||
},
|
||||
isTelemetryEnabledOnRoute(enabled) {
|
||||
if (enabled) {
|
||||
this.init();
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.init();
|
||||
},
|
||||
methods: {
|
||||
init() {
|
||||
if (
|
||||
this.isTelemetryInitialized ||
|
||||
!this.isTelemetryEnabledOnRoute ||
|
||||
!this.isTelemetryEnabled
|
||||
)
|
||||
return;
|
||||
const isTelemetryInitialized = ref(false);
|
||||
|
||||
this.$telemetry.init(this.telemetry, {
|
||||
instanceId: this.rootStore.instanceId,
|
||||
userId: this.currentUserId,
|
||||
projectId: this.projectsStore.personalProject?.id,
|
||||
versionCli: this.rootStore.versionCli,
|
||||
});
|
||||
const rootStore = useRootStore();
|
||||
const settingsStore = useSettingsStore();
|
||||
const usersStore = useUsersStore();
|
||||
const projectsStore = useProjectsStore();
|
||||
const telemetryPlugin = useTelemetry();
|
||||
const route = useRoute();
|
||||
|
||||
this.isTelemetryInitialized = true;
|
||||
},
|
||||
},
|
||||
const currentUserId = computed((): string => {
|
||||
return usersStore.currentUserId ?? '';
|
||||
});
|
||||
|
||||
const isTelemetryEnabledOnRoute = computed((): boolean => {
|
||||
const routeMeta = route.meta as { telemetry?: { disabled?: boolean } } | undefined;
|
||||
return routeMeta?.telemetry ? !routeMeta.telemetry.disabled : true;
|
||||
});
|
||||
|
||||
const telemetry = computed((): ITelemetrySettings => {
|
||||
return settingsStore.telemetry;
|
||||
});
|
||||
|
||||
const isTelemetryEnabled = computed((): boolean => {
|
||||
return !!telemetry.value?.enabled;
|
||||
});
|
||||
|
||||
watch(telemetry, () => {
|
||||
init();
|
||||
});
|
||||
|
||||
watch(currentUserId, (userId) => {
|
||||
if (isTelemetryEnabled.value) {
|
||||
telemetryPlugin.identify(rootStore.instanceId, userId);
|
||||
}
|
||||
});
|
||||
|
||||
watch(isTelemetryEnabledOnRoute, (enabled) => {
|
||||
if (enabled) {
|
||||
init();
|
||||
}
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
init();
|
||||
});
|
||||
|
||||
function init() {
|
||||
if (isTelemetryInitialized.value || !isTelemetryEnabledOnRoute.value || !isTelemetryEnabled.value)
|
||||
return;
|
||||
|
||||
telemetryPlugin.init(telemetry.value, {
|
||||
instanceId: rootStore.instanceId,
|
||||
userId: currentUserId.value,
|
||||
projectId: projectsStore.personalProject?.id,
|
||||
versionCli: rootStore.versionCli,
|
||||
});
|
||||
|
||||
isTelemetryInitialized.value = true;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span v-show="false" />
|
||||
</template>
|
||||
|
|
|
@ -1,3 +1,84 @@
|
|||
<script lang="ts" setup>
|
||||
import { useToast } from '@/composables/useToast';
|
||||
import { useWorkflowActivate } from '@/composables/useWorkflowActivate';
|
||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||
import { getActivatableTriggerNodes } from '@/utils/nodeTypesUtils';
|
||||
import { computed } from 'vue';
|
||||
import { useI18n } from '@/composables/useI18n';
|
||||
|
||||
const props = defineProps<{ workflowActive: boolean; workflowId: string }>();
|
||||
const { showMessage } = useToast();
|
||||
const workflowActivate = useWorkflowActivate();
|
||||
|
||||
const i18n = useI18n();
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
|
||||
const isWorkflowActive = computed((): boolean => {
|
||||
const activeWorkflows = workflowsStore.activeWorkflows;
|
||||
return activeWorkflows.includes(props.workflowId);
|
||||
});
|
||||
const couldNotBeStarted = computed((): boolean => {
|
||||
return props.workflowActive && isWorkflowActive.value !== props.workflowActive;
|
||||
});
|
||||
const getActiveColor = computed((): string => {
|
||||
if (couldNotBeStarted.value) {
|
||||
return '#ff4949';
|
||||
}
|
||||
return '#13ce66';
|
||||
});
|
||||
const isCurrentWorkflow = computed((): boolean => {
|
||||
return workflowsStore.workflowId === props.workflowId;
|
||||
});
|
||||
|
||||
const containsTrigger = computed((): boolean => {
|
||||
const foundTriggers = getActivatableTriggerNodes(workflowsStore.workflowTriggerNodes);
|
||||
return foundTriggers.length > 0;
|
||||
});
|
||||
|
||||
const disabled = computed((): boolean => {
|
||||
const isNewWorkflow = !props.workflowId;
|
||||
if (isNewWorkflow || isCurrentWorkflow.value) {
|
||||
return !props.workflowActive && !containsTrigger.value;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
async function activeChanged(newActiveState: boolean) {
|
||||
return await workflowActivate.updateWorkflowActivation(props.workflowId, newActiveState);
|
||||
}
|
||||
|
||||
async function displayActivationError() {
|
||||
let errorMessage: string;
|
||||
try {
|
||||
const errorData = await workflowsStore.getActivationError(props.workflowId);
|
||||
|
||||
if (errorData === undefined) {
|
||||
errorMessage = i18n.baseText(
|
||||
'workflowActivator.showMessage.displayActivationError.message.errorDataUndefined',
|
||||
);
|
||||
} else {
|
||||
errorMessage = i18n.baseText(
|
||||
'workflowActivator.showMessage.displayActivationError.message.errorDataNotUndefined',
|
||||
{ interpolate: { message: errorData } },
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
errorMessage = i18n.baseText(
|
||||
'workflowActivator.showMessage.displayActivationError.message.catchBlock',
|
||||
);
|
||||
}
|
||||
|
||||
showMessage({
|
||||
title: i18n.baseText('workflowActivator.showMessage.displayActivationError.title'),
|
||||
message: errorMessage,
|
||||
type: 'warning',
|
||||
duration: 0,
|
||||
dangerouslyUseHTMLString: true,
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="workflow-activator">
|
||||
<div :class="$style.activeStatusText" data-test-id="workflow-activator-status">
|
||||
|
@ -7,27 +88,27 @@
|
|||
size="small"
|
||||
bold
|
||||
>
|
||||
{{ $locale.baseText('workflowActivator.active') }}
|
||||
{{ i18n.baseText('workflowActivator.active') }}
|
||||
</n8n-text>
|
||||
<n8n-text v-else color="text-base" size="small" bold>
|
||||
{{ $locale.baseText('workflowActivator.inactive') }}
|
||||
{{ i18n.baseText('workflowActivator.inactive') }}
|
||||
</n8n-text>
|
||||
</div>
|
||||
<n8n-tooltip :disabled="!disabled" placement="bottom">
|
||||
<template #content>
|
||||
<div>
|
||||
{{ $locale.baseText('workflowActivator.thisWorkflowHasNoTriggerNodes') }}
|
||||
{{ i18n.baseText('workflowActivator.thisWorkflowHasNoTriggerNodes') }}
|
||||
</div>
|
||||
</template>
|
||||
<el-switch
|
||||
v-loading="updatingWorkflowActivation"
|
||||
v-loading="workflowActivate.updatingWorkflowActivation.value"
|
||||
:model-value="workflowActive"
|
||||
:title="
|
||||
workflowActive
|
||||
? $locale.baseText('workflowActivator.deactivateWorkflow')
|
||||
: $locale.baseText('workflowActivator.activateWorkflow')
|
||||
? i18n.baseText('workflowActivator.deactivateWorkflow')
|
||||
: i18n.baseText('workflowActivator.activateWorkflow')
|
||||
"
|
||||
:disabled="disabled || updatingWorkflowActivation"
|
||||
:disabled="disabled || workflowActivate.updatingWorkflowActivation.value"
|
||||
:active-color="getActiveColor"
|
||||
inactive-color="#8899AA"
|
||||
data-test-id="workflow-activate-switch"
|
||||
|
@ -41,7 +122,7 @@
|
|||
<template #content>
|
||||
<div
|
||||
@click="displayActivationError"
|
||||
v-html="$locale.baseText('workflowActivator.theWorkflowIsSetToBeActiveBut')"
|
||||
v-html="i18n.baseText('workflowActivator.theWorkflowIsSetToBeActiveBut')"
|
||||
></div>
|
||||
</template>
|
||||
<font-awesome-icon icon="exclamation-triangle" @click="displayActivationError" />
|
||||
|
@ -50,95 +131,6 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { useToast } from '@/composables/useToast';
|
||||
import { useWorkflowActivate } from '@/composables/useWorkflowActivate';
|
||||
import { useUIStore } from '@/stores/ui.store';
|
||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||
import { mapStores } from 'pinia';
|
||||
import { defineComponent } from 'vue';
|
||||
import { getActivatableTriggerNodes } from '@/utils/nodeTypesUtils';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'WorkflowActivator',
|
||||
props: ['workflowActive', 'workflowId'],
|
||||
setup() {
|
||||
return {
|
||||
...useToast(),
|
||||
...useWorkflowActivate(),
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapStores(useUIStore, useWorkflowsStore),
|
||||
nodesIssuesExist(): boolean {
|
||||
return this.workflowsStore.nodesIssuesExist;
|
||||
},
|
||||
isWorkflowActive(): boolean {
|
||||
const activeWorkflows = this.workflowsStore.activeWorkflows;
|
||||
return activeWorkflows.includes(this.workflowId);
|
||||
},
|
||||
couldNotBeStarted(): boolean {
|
||||
return this.workflowActive === true && this.isWorkflowActive !== this.workflowActive;
|
||||
},
|
||||
getActiveColor(): string {
|
||||
if (this.couldNotBeStarted) {
|
||||
return '#ff4949';
|
||||
}
|
||||
return '#13ce66';
|
||||
},
|
||||
isCurrentWorkflow(): boolean {
|
||||
return this.workflowsStore.workflowId === this.workflowId;
|
||||
},
|
||||
disabled(): boolean {
|
||||
const isNewWorkflow = !this.workflowId;
|
||||
if (isNewWorkflow || this.isCurrentWorkflow) {
|
||||
return !this.workflowActive && !this.containsTrigger;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
containsTrigger(): boolean {
|
||||
const foundTriggers = getActivatableTriggerNodes(this.workflowsStore.workflowTriggerNodes);
|
||||
return foundTriggers.length > 0;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async activeChanged(newActiveState: boolean) {
|
||||
return await this.updateWorkflowActivation(this.workflowId, newActiveState);
|
||||
},
|
||||
async displayActivationError() {
|
||||
let errorMessage: string;
|
||||
try {
|
||||
const errorData = await this.workflowsStore.getActivationError(this.workflowId);
|
||||
|
||||
if (errorData === undefined) {
|
||||
errorMessage = this.$locale.baseText(
|
||||
'workflowActivator.showMessage.displayActivationError.message.errorDataUndefined',
|
||||
);
|
||||
} else {
|
||||
errorMessage = this.$locale.baseText(
|
||||
'workflowActivator.showMessage.displayActivationError.message.errorDataNotUndefined',
|
||||
{ interpolate: { message: errorData } },
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
errorMessage = this.$locale.baseText(
|
||||
'workflowActivator.showMessage.displayActivationError.message.catchBlock',
|
||||
);
|
||||
}
|
||||
|
||||
this.showMessage({
|
||||
title: this.$locale.baseText('workflowActivator.showMessage.displayActivationError.title'),
|
||||
message: errorMessage,
|
||||
type: 'warning',
|
||||
duration: 0,
|
||||
dangerouslyUseHTMLString: true,
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.activeStatusText {
|
||||
width: 64px; // Required to avoid jumping when changing active state
|
||||
|
|
|
@ -1,46 +1,45 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, onBeforeUnmount, defineProps } from 'vue';
|
||||
import { useI18n } from '@/composables/useI18n';
|
||||
|
||||
const props = defineProps<{
|
||||
startTime: Date;
|
||||
}>();
|
||||
|
||||
const i18n = useI18n();
|
||||
|
||||
const nowTime = ref(-1);
|
||||
const intervalTimer = ref<NodeJS.Timeout | null>(null);
|
||||
|
||||
const time = computed(() => {
|
||||
if (!props.startTime) {
|
||||
return '...';
|
||||
}
|
||||
const msPassed = nowTime.value - new Date(props.startTime).getTime();
|
||||
return i18n.displayTimer(msPassed); // Note: Adjust for $locale usage in setup
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
setNow();
|
||||
intervalTimer.value = setInterval(() => {
|
||||
setNow();
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
// Make sure that the timer gets destroyed once no longer needed
|
||||
if (intervalTimer.value !== null) {
|
||||
clearInterval(intervalTimer.value);
|
||||
}
|
||||
});
|
||||
|
||||
function setNow() {
|
||||
nowTime.value = new Date().getTime();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span>
|
||||
{{ time }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ExecutionsTime',
|
||||
props: ['startTime'],
|
||||
data() {
|
||||
return {
|
||||
nowTime: -1,
|
||||
intervalTimer: null as null | NodeJS.Timeout,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
time(): string {
|
||||
if (!this.startTime) {
|
||||
return '...';
|
||||
}
|
||||
const msPassed = this.nowTime - new Date(this.startTime).getTime();
|
||||
return this.$locale.displayTimer(msPassed);
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.setNow();
|
||||
this.intervalTimer = setInterval(() => {
|
||||
this.setNow();
|
||||
}, 1000);
|
||||
},
|
||||
beforeUnmount() {
|
||||
// Make sure that the timer gets destroyed once no longer needed
|
||||
if (this.intervalTimer !== null) {
|
||||
clearInterval(this.intervalTimer);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
setNow() {
|
||||
this.nowTime = new Date().getTime();
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -1,3 +1,174 @@
|
|||
<script lang="ts" setup>
|
||||
import { useI18n } from '@/composables/useI18n';
|
||||
import { useToast } from '@/composables/useToast';
|
||||
import type { IFormInputs, IUser, ThemeOption } from '@/Interface';
|
||||
import { CHANGE_PASSWORD_MODAL_KEY, MFA_DOCS_URL, MFA_SETUP_MODAL_KEY } from '@/constants';
|
||||
import { useUIStore } from '@/stores/ui.store';
|
||||
import { useUsersStore } from '@/stores/users.store';
|
||||
import { useSettingsStore } from '@/stores/settings.store';
|
||||
import { createEventBus } from 'n8n-design-system/utils';
|
||||
import { ref } from 'vue';
|
||||
import { computed } from 'vue';
|
||||
import { onMounted } from 'vue';
|
||||
|
||||
const i18n = useI18n();
|
||||
const { showToast, showError } = useToast();
|
||||
|
||||
const hasAnyBasicInfoChanges = ref<boolean>(false);
|
||||
const formInputs = ref<null | IFormInputs>(null);
|
||||
const formBus = ref(createEventBus());
|
||||
const readyToSubmit = ref(false);
|
||||
const currentSelectedTheme = ref(useUIStore().theme);
|
||||
const themeOptions = ref<Array<{ name: ThemeOption; label: string }>>([
|
||||
{
|
||||
name: 'system',
|
||||
label: 'settings.personal.theme.systemDefault',
|
||||
},
|
||||
{
|
||||
name: 'light',
|
||||
label: 'settings.personal.theme.light',
|
||||
},
|
||||
{
|
||||
name: 'dark',
|
||||
label: 'settings.personal.theme.dark',
|
||||
},
|
||||
]);
|
||||
|
||||
const uiStore = useUIStore();
|
||||
const usersStore = useUsersStore();
|
||||
const settingsStore = useSettingsStore();
|
||||
|
||||
const currentUser = computed((): IUser | null => {
|
||||
return usersStore.currentUser;
|
||||
});
|
||||
const isExternalAuthEnabled = computed((): boolean => {
|
||||
const isLdapEnabled =
|
||||
settingsStore.settings.enterprise.ldap && currentUser.value?.signInType === 'ldap';
|
||||
const isSamlEnabled =
|
||||
settingsStore.isSamlLoginEnabled && settingsStore.isDefaultAuthenticationSaml;
|
||||
return isLdapEnabled || isSamlEnabled;
|
||||
});
|
||||
const isPersonalSecurityEnabled = computed((): boolean => {
|
||||
return usersStore.isInstanceOwner || !isExternalAuthEnabled.value;
|
||||
});
|
||||
const mfaDisabled = computed((): boolean => {
|
||||
return !usersStore.mfaEnabled;
|
||||
});
|
||||
const isMfaFeatureEnabled = computed((): boolean => {
|
||||
return settingsStore.isMfaFeatureEnabled;
|
||||
});
|
||||
const hasAnyPersonalisationChanges = computed((): boolean => {
|
||||
return currentSelectedTheme.value !== uiStore.theme;
|
||||
});
|
||||
const hasAnyChanges = computed(() => {
|
||||
return hasAnyBasicInfoChanges.value || hasAnyPersonalisationChanges.value;
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
formInputs.value = [
|
||||
{
|
||||
name: 'firstName',
|
||||
initialValue: currentUser.value?.firstName,
|
||||
properties: {
|
||||
label: i18n.baseText('auth.firstName'),
|
||||
maxlength: 32,
|
||||
required: true,
|
||||
autocomplete: 'given-name',
|
||||
capitalize: true,
|
||||
disabled: isExternalAuthEnabled.value,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'lastName',
|
||||
initialValue: currentUser.value?.lastName,
|
||||
properties: {
|
||||
label: i18n.baseText('auth.lastName'),
|
||||
maxlength: 32,
|
||||
required: true,
|
||||
autocomplete: 'family-name',
|
||||
capitalize: true,
|
||||
disabled: isExternalAuthEnabled.value,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'email',
|
||||
initialValue: currentUser.value?.email,
|
||||
properties: {
|
||||
label: i18n.baseText('auth.email'),
|
||||
type: 'email',
|
||||
required: true,
|
||||
validationRules: [{ name: 'VALID_EMAIL' }],
|
||||
autocomplete: 'email',
|
||||
capitalize: true,
|
||||
disabled: !isPersonalSecurityEnabled.value,
|
||||
},
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
function onInput() {
|
||||
hasAnyBasicInfoChanges.value = true;
|
||||
}
|
||||
function onReadyToSubmit(ready: boolean) {
|
||||
readyToSubmit.value = ready;
|
||||
}
|
||||
async function onSubmit(form: { firstName: string; lastName: string; email: string }) {
|
||||
try {
|
||||
await Promise.all([updateUserBasicInfo(form), updatePersonalisationSettings()]);
|
||||
|
||||
showToast({
|
||||
title: i18n.baseText('settings.personal.personalSettingsUpdated'),
|
||||
message: '',
|
||||
type: 'success',
|
||||
});
|
||||
} catch (e) {
|
||||
showError(e, i18n.baseText('settings.personal.personalSettingsUpdatedError'));
|
||||
}
|
||||
}
|
||||
async function updateUserBasicInfo(form: { firstName: string; lastName: string; email: string }) {
|
||||
if (!hasAnyBasicInfoChanges.value || !usersStore.currentUserId) {
|
||||
return;
|
||||
}
|
||||
|
||||
await usersStore.updateUser({
|
||||
id: usersStore.currentUserId,
|
||||
firstName: form.firstName,
|
||||
lastName: form.lastName,
|
||||
email: form.email,
|
||||
});
|
||||
hasAnyBasicInfoChanges.value = false;
|
||||
}
|
||||
async function updatePersonalisationSettings() {
|
||||
if (!hasAnyPersonalisationChanges.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
uiStore.setTheme(currentSelectedTheme.value);
|
||||
}
|
||||
function onSaveClick() {
|
||||
formBus.value.emit('submit');
|
||||
}
|
||||
function openPasswordModal() {
|
||||
uiStore.openModal(CHANGE_PASSWORD_MODAL_KEY);
|
||||
}
|
||||
function onMfaEnableClick() {
|
||||
uiStore.openModal(MFA_SETUP_MODAL_KEY);
|
||||
}
|
||||
async function onMfaDisableClick() {
|
||||
try {
|
||||
await usersStore.disabledMfa();
|
||||
showToast({
|
||||
title: i18n.baseText('settings.personal.mfa.toast.disabledMfa.title'),
|
||||
message: i18n.baseText('settings.personal.mfa.toast.disabledMfa.message'),
|
||||
type: 'success',
|
||||
duration: 0,
|
||||
});
|
||||
} catch (e) {
|
||||
showError(e, i18n.baseText('settings.personal.mfa.toast.disabledMfa.error.message'));
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="$style.container" data-test-id="personal-settings-container">
|
||||
<div :class="$style.header">
|
||||
|
@ -45,15 +216,15 @@
|
|||
</div>
|
||||
<div v-if="isMfaFeatureEnabled" data-test-id="mfa-section">
|
||||
<div class="mb-xs">
|
||||
<n8n-input-label :label="$locale.baseText('settings.personal.mfa.section.title')" />
|
||||
<n8n-input-label :label="i18n.baseText('settings.personal.mfa.section.title')" />
|
||||
<n8n-text :bold="false" :class="$style.infoText">
|
||||
{{
|
||||
mfaDisabled
|
||||
? $locale.baseText('settings.personal.mfa.button.disabled.infobox')
|
||||
: $locale.baseText('settings.personal.mfa.button.enabled.infobox')
|
||||
? i18n.baseText('settings.personal.mfa.button.disabled.infobox')
|
||||
: i18n.baseText('settings.personal.mfa.button.enabled.infobox')
|
||||
}}
|
||||
<n8n-link :to="mfaDocsUrl" size="small" :bold="true">
|
||||
{{ $locale.baseText('generic.learnMore') }}
|
||||
<n8n-link :to="MFA_DOCS_URL" size="small" :bold="true">
|
||||
{{ i18n.baseText('generic.learnMore') }}
|
||||
</n8n-link>
|
||||
</n8n-text>
|
||||
</div>
|
||||
|
@ -61,7 +232,7 @@
|
|||
v-if="mfaDisabled"
|
||||
:class="$style.button"
|
||||
type="tertiary"
|
||||
:label="$locale.baseText('settings.personal.mfa.button.enabled')"
|
||||
:label="i18n.baseText('settings.personal.mfa.button.enabled')"
|
||||
data-test-id="enable-mfa-button"
|
||||
@click="onMfaEnableClick"
|
||||
/>
|
||||
|
@ -69,7 +240,7 @@
|
|||
v-else
|
||||
:class="$style.disableMfaButton"
|
||||
type="tertiary"
|
||||
:label="$locale.baseText('settings.personal.mfa.button.disabled')"
|
||||
:label="i18n.baseText('settings.personal.mfa.button.disabled')"
|
||||
data-test-id="disable-mfa-button"
|
||||
@click="onMfaDisableClick"
|
||||
/>
|
||||
|
@ -114,190 +285,6 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { useI18n } from '@/composables/useI18n';
|
||||
import { useToast } from '@/composables/useToast';
|
||||
import type { IFormInputs, IUser, ThemeOption } from '@/Interface';
|
||||
import { CHANGE_PASSWORD_MODAL_KEY, MFA_DOCS_URL, MFA_SETUP_MODAL_KEY } from '@/constants';
|
||||
import { useUIStore } from '@/stores/ui.store';
|
||||
import { useUsersStore } from '@/stores/users.store';
|
||||
import { useSettingsStore } from '@/stores/settings.store';
|
||||
import { mapStores } from 'pinia';
|
||||
import { defineComponent } from 'vue';
|
||||
import { createEventBus } from 'n8n-design-system/utils';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'SettingsPersonalView',
|
||||
setup() {
|
||||
const i18n = useI18n();
|
||||
|
||||
return {
|
||||
i18n,
|
||||
...useToast(),
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
hasAnyBasicInfoChanges: false,
|
||||
formInputs: null as null | IFormInputs,
|
||||
formBus: createEventBus(),
|
||||
readyToSubmit: false,
|
||||
mfaDocsUrl: MFA_DOCS_URL,
|
||||
currentSelectedTheme: useUIStore().theme,
|
||||
themeOptions: [
|
||||
{
|
||||
name: 'system',
|
||||
label: 'settings.personal.theme.systemDefault',
|
||||
},
|
||||
{
|
||||
name: 'light',
|
||||
label: 'settings.personal.theme.light',
|
||||
},
|
||||
{
|
||||
name: 'dark',
|
||||
label: 'settings.personal.theme.dark',
|
||||
},
|
||||
] as Array<{ name: ThemeOption; label: string }>,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapStores(useUIStore, useUsersStore, useSettingsStore),
|
||||
currentUser(): IUser | null {
|
||||
return this.usersStore.currentUser;
|
||||
},
|
||||
isExternalAuthEnabled(): boolean {
|
||||
const isLdapEnabled =
|
||||
this.settingsStore.settings.enterprise.ldap && this.currentUser?.signInType === 'ldap';
|
||||
const isSamlEnabled =
|
||||
this.settingsStore.isSamlLoginEnabled && this.settingsStore.isDefaultAuthenticationSaml;
|
||||
return isLdapEnabled || isSamlEnabled;
|
||||
},
|
||||
isPersonalSecurityEnabled(): boolean {
|
||||
return this.usersStore.isInstanceOwner || !this.isExternalAuthEnabled;
|
||||
},
|
||||
mfaDisabled(): boolean {
|
||||
return !this.usersStore.mfaEnabled;
|
||||
},
|
||||
isMfaFeatureEnabled(): boolean {
|
||||
return this.settingsStore.isMfaFeatureEnabled;
|
||||
},
|
||||
hasAnyPersonalisationChanges(): boolean {
|
||||
return this.currentSelectedTheme !== this.uiStore.theme;
|
||||
},
|
||||
hasAnyChanges() {
|
||||
return this.hasAnyBasicInfoChanges || this.hasAnyPersonalisationChanges;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.formInputs = [
|
||||
{
|
||||
name: 'firstName',
|
||||
initialValue: this.currentUser?.firstName,
|
||||
properties: {
|
||||
label: this.i18n.baseText('auth.firstName'),
|
||||
maxlength: 32,
|
||||
required: true,
|
||||
autocomplete: 'given-name',
|
||||
capitalize: true,
|
||||
disabled: this.isExternalAuthEnabled,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'lastName',
|
||||
initialValue: this.currentUser?.lastName,
|
||||
properties: {
|
||||
label: this.i18n.baseText('auth.lastName'),
|
||||
maxlength: 32,
|
||||
required: true,
|
||||
autocomplete: 'family-name',
|
||||
capitalize: true,
|
||||
disabled: this.isExternalAuthEnabled,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'email',
|
||||
initialValue: this.currentUser?.email,
|
||||
properties: {
|
||||
label: this.i18n.baseText('auth.email'),
|
||||
type: 'email',
|
||||
required: true,
|
||||
validationRules: [{ name: 'VALID_EMAIL' }],
|
||||
autocomplete: 'email',
|
||||
capitalize: true,
|
||||
disabled: !this.isPersonalSecurityEnabled,
|
||||
},
|
||||
},
|
||||
];
|
||||
},
|
||||
methods: {
|
||||
onInput() {
|
||||
this.hasAnyBasicInfoChanges = true;
|
||||
},
|
||||
onReadyToSubmit(ready: boolean) {
|
||||
this.readyToSubmit = ready;
|
||||
},
|
||||
async onSubmit(form: { firstName: string; lastName: string; email: string }) {
|
||||
try {
|
||||
await Promise.all([this.updateUserBasicInfo(form), this.updatePersonalisationSettings()]);
|
||||
|
||||
this.showToast({
|
||||
title: this.i18n.baseText('settings.personal.personalSettingsUpdated'),
|
||||
message: '',
|
||||
type: 'success',
|
||||
});
|
||||
} catch (e) {
|
||||
this.showError(e, this.i18n.baseText('settings.personal.personalSettingsUpdatedError'));
|
||||
}
|
||||
},
|
||||
async updateUserBasicInfo(form: { firstName: string; lastName: string; email: string }) {
|
||||
if (!this.hasAnyBasicInfoChanges || !this.usersStore.currentUserId) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.usersStore.updateUser({
|
||||
id: this.usersStore.currentUserId,
|
||||
firstName: form.firstName,
|
||||
lastName: form.lastName,
|
||||
email: form.email,
|
||||
});
|
||||
this.hasAnyBasicInfoChanges = false;
|
||||
},
|
||||
async updatePersonalisationSettings() {
|
||||
if (!this.hasAnyPersonalisationChanges) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.uiStore.setTheme(this.currentSelectedTheme);
|
||||
},
|
||||
onSaveClick() {
|
||||
this.formBus.emit('submit');
|
||||
},
|
||||
openPasswordModal() {
|
||||
this.uiStore.openModal(CHANGE_PASSWORD_MODAL_KEY);
|
||||
},
|
||||
onMfaEnableClick() {
|
||||
this.uiStore.openModal(MFA_SETUP_MODAL_KEY);
|
||||
},
|
||||
async onMfaDisableClick() {
|
||||
try {
|
||||
await this.usersStore.disabledMfa();
|
||||
this.showToast({
|
||||
title: this.$locale.baseText('settings.personal.mfa.toast.disabledMfa.title'),
|
||||
message: this.$locale.baseText('settings.personal.mfa.toast.disabledMfa.message'),
|
||||
type: 'success',
|
||||
duration: 0,
|
||||
});
|
||||
} catch (e) {
|
||||
this.showError(
|
||||
e,
|
||||
this.$locale.baseText('settings.personal.mfa.toast.disabledMfa.error.message'),
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.container {
|
||||
> * {
|
||||
|
|
|
@ -1,16 +1,233 @@
|
|||
<script lang="ts" setup>
|
||||
import { EnterpriseEditionFeature, INVITE_USER_MODAL_KEY, ROLE } from '@/constants';
|
||||
|
||||
import type { IRole, IUser, IUserListAction, InvitableRoleName } from '@/Interface';
|
||||
import { useToast } from '@/composables/useToast';
|
||||
import { useUIStore } from '@/stores/ui.store';
|
||||
import { useSettingsStore } from '@/stores/settings.store';
|
||||
import { useUsersStore } from '@/stores/users.store';
|
||||
import { useSSOStore } from '@/stores/sso.store';
|
||||
import { hasPermission } from '@/utils/rbac/permissions';
|
||||
import { useClipboard } from '@/composables/useClipboard';
|
||||
import type { UpdateGlobalRolePayload } from '@/api/users';
|
||||
import { computed, onMounted } from 'vue';
|
||||
import { useI18n } from '@/composables/useI18n';
|
||||
|
||||
const clipboard = useClipboard();
|
||||
const { showToast, showError } = useToast();
|
||||
|
||||
const settingsStore = useSettingsStore();
|
||||
const uiStore = useUIStore();
|
||||
const usersStore = useUsersStore();
|
||||
const ssoStore = useSSOStore();
|
||||
|
||||
const i18n = useI18n();
|
||||
|
||||
const showUMSetupWarning = computed(() => {
|
||||
return hasPermission(['defaultUser']);
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
if (!showUMSetupWarning.value) {
|
||||
await usersStore.fetchUsers();
|
||||
}
|
||||
});
|
||||
|
||||
const usersListActions = computed((): IUserListAction[] => {
|
||||
return [
|
||||
{
|
||||
label: i18n.baseText('settings.users.actions.copyInviteLink'),
|
||||
value: 'copyInviteLink',
|
||||
guard: (user) => settingsStore.isBelowUserQuota && !user.firstName && !!user.inviteAcceptUrl,
|
||||
},
|
||||
{
|
||||
label: i18n.baseText('settings.users.actions.reinvite'),
|
||||
value: 'reinvite',
|
||||
guard: (user) =>
|
||||
settingsStore.isBelowUserQuota && !user.firstName && settingsStore.isSmtpSetup,
|
||||
},
|
||||
{
|
||||
label: i18n.baseText('settings.users.actions.delete'),
|
||||
value: 'delete',
|
||||
guard: (user) =>
|
||||
hasPermission(['rbac'], { rbac: { scope: 'user:delete' } }) &&
|
||||
user.id !== usersStore.currentUserId,
|
||||
},
|
||||
{
|
||||
label: i18n.baseText('settings.users.actions.copyPasswordResetLink'),
|
||||
value: 'copyPasswordResetLink',
|
||||
guard: (user) =>
|
||||
hasPermission(['rbac'], { rbac: { scope: 'user:resetPassword' } }) &&
|
||||
settingsStore.isBelowUserQuota &&
|
||||
!user.isPendingUser &&
|
||||
user.id !== usersStore.currentUserId,
|
||||
},
|
||||
{
|
||||
label: i18n.baseText('settings.users.actions.allowSSOManualLogin'),
|
||||
value: 'allowSSOManualLogin',
|
||||
guard: (user) => settingsStore.isSamlLoginEnabled && !user.settings?.allowSSOManualLogin,
|
||||
},
|
||||
{
|
||||
label: i18n.baseText('settings.users.actions.disallowSSOManualLogin'),
|
||||
value: 'disallowSSOManualLogin',
|
||||
guard: (user) =>
|
||||
settingsStore.isSamlLoginEnabled && user.settings?.allowSSOManualLogin === true,
|
||||
},
|
||||
];
|
||||
});
|
||||
const isAdvancedPermissionsEnabled = computed((): boolean => {
|
||||
return settingsStore.isEnterpriseFeatureEnabled(EnterpriseEditionFeature.AdvancedPermissions);
|
||||
});
|
||||
|
||||
const userRoles = computed((): Array<{ value: IRole; label: string; disabled?: boolean }> => {
|
||||
return [
|
||||
{
|
||||
value: ROLE.Member,
|
||||
label: i18n.baseText('auth.roles.member'),
|
||||
},
|
||||
{
|
||||
value: ROLE.Admin,
|
||||
label: i18n.baseText('auth.roles.admin'),
|
||||
disabled: !isAdvancedPermissionsEnabled.value,
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
const canUpdateRole = computed((): boolean => {
|
||||
return hasPermission(['rbac'], { rbac: { scope: ['user:update', 'user:changeRole'] } });
|
||||
});
|
||||
|
||||
async function onUsersListAction({ action, userId }: { action: string; userId: string }) {
|
||||
switch (action) {
|
||||
case 'delete':
|
||||
await onDelete(userId);
|
||||
break;
|
||||
case 'reinvite':
|
||||
await onReinvite(userId);
|
||||
break;
|
||||
case 'copyInviteLink':
|
||||
await onCopyInviteLink(userId);
|
||||
break;
|
||||
case 'copyPasswordResetLink':
|
||||
await onCopyPasswordResetLink(userId);
|
||||
break;
|
||||
case 'allowSSOManualLogin':
|
||||
await onAllowSSOManualLogin(userId);
|
||||
break;
|
||||
case 'disallowSSOManualLogin':
|
||||
await onDisallowSSOManualLogin(userId);
|
||||
break;
|
||||
}
|
||||
}
|
||||
function onInvite() {
|
||||
uiStore.openModal(INVITE_USER_MODAL_KEY);
|
||||
}
|
||||
async function onDelete(userId: string) {
|
||||
const user = usersStore.usersById[userId];
|
||||
if (user) {
|
||||
uiStore.openDeleteUserModal(userId);
|
||||
}
|
||||
}
|
||||
async function onReinvite(userId: string) {
|
||||
const user = usersStore.usersById[userId];
|
||||
if (user?.email && user?.role) {
|
||||
if (!['global:admin', 'global:member'].includes(user.role)) {
|
||||
throw new Error('Invalid role name on reinvite');
|
||||
}
|
||||
try {
|
||||
await usersStore.reinviteUser({
|
||||
email: user.email,
|
||||
role: user.role as InvitableRoleName,
|
||||
});
|
||||
showToast({
|
||||
type: 'success',
|
||||
title: i18n.baseText('settings.users.inviteResent'),
|
||||
message: i18n.baseText('settings.users.emailSentTo', {
|
||||
interpolate: { email: user.email ?? '' },
|
||||
}),
|
||||
});
|
||||
} catch (e) {
|
||||
showError(e, i18n.baseText('settings.users.userReinviteError'));
|
||||
}
|
||||
}
|
||||
}
|
||||
async function onCopyInviteLink(userId: string) {
|
||||
const user = usersStore.usersById[userId];
|
||||
if (user?.inviteAcceptUrl) {
|
||||
void clipboard.copy(user.inviteAcceptUrl);
|
||||
|
||||
showToast({
|
||||
type: 'success',
|
||||
title: i18n.baseText('settings.users.inviteUrlCreated'),
|
||||
message: i18n.baseText('settings.users.inviteUrlCreated.message'),
|
||||
});
|
||||
}
|
||||
}
|
||||
async function onCopyPasswordResetLink(userId: string) {
|
||||
const user = usersStore.usersById[userId];
|
||||
if (user) {
|
||||
const url = await usersStore.getUserPasswordResetLink(user);
|
||||
void clipboard.copy(url.link);
|
||||
|
||||
showToast({
|
||||
type: 'success',
|
||||
title: i18n.baseText('settings.users.passwordResetUrlCreated'),
|
||||
message: i18n.baseText('settings.users.passwordResetUrlCreated.message'),
|
||||
});
|
||||
}
|
||||
}
|
||||
async function onAllowSSOManualLogin(userId: string) {
|
||||
const user = usersStore.usersById[userId];
|
||||
if (user) {
|
||||
if (!user.settings) {
|
||||
user.settings = {};
|
||||
}
|
||||
user.settings.allowSSOManualLogin = true;
|
||||
await usersStore.updateOtherUserSettings(userId, user.settings);
|
||||
|
||||
showToast({
|
||||
type: 'success',
|
||||
title: i18n.baseText('settings.users.allowSSOManualLogin'),
|
||||
message: i18n.baseText('settings.users.allowSSOManualLogin.message'),
|
||||
});
|
||||
}
|
||||
}
|
||||
async function onDisallowSSOManualLogin(userId: string) {
|
||||
const user = usersStore.usersById[userId];
|
||||
if (user?.settings) {
|
||||
user.settings.allowSSOManualLogin = false;
|
||||
await usersStore.updateOtherUserSettings(userId, user.settings);
|
||||
showToast({
|
||||
type: 'success',
|
||||
title: i18n.baseText('settings.users.disallowSSOManualLogin'),
|
||||
message: i18n.baseText('settings.users.disallowSSOManualLogin.message'),
|
||||
});
|
||||
}
|
||||
}
|
||||
function goToUpgrade() {
|
||||
void uiStore.goToUpgrade('settings-users', 'upgrade-users');
|
||||
}
|
||||
function goToUpgradeAdvancedPermissions() {
|
||||
void uiStore.goToUpgrade('settings-users', 'upgrade-advanced-permissions');
|
||||
}
|
||||
async function onRoleChange(user: IUser, newRoleName: UpdateGlobalRolePayload['newRoleName']) {
|
||||
await usersStore.updateGlobalRole({ id: user.id, newRoleName });
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="$style.container">
|
||||
<div>
|
||||
<n8n-heading size="2xlarge">{{ $locale.baseText('settings.users') }}</n8n-heading>
|
||||
<n8n-heading size="2xlarge">{{ i18n.baseText('settings.users') }}</n8n-heading>
|
||||
<div v-if="!showUMSetupWarning" :class="$style.buttonContainer">
|
||||
<n8n-tooltip :disabled="!ssoStore.isSamlLoginEnabled">
|
||||
<template #content>
|
||||
<span> {{ $locale.baseText('settings.users.invite.tooltip') }} </span>
|
||||
<span> {{ i18n.baseText('settings.users.invite.tooltip') }} </span>
|
||||
</template>
|
||||
<div>
|
||||
<n8n-button
|
||||
:disabled="ssoStore.isSamlLoginEnabled || !settingsStore.isBelowUserQuota"
|
||||
:label="$locale.baseText('settings.users.invite')"
|
||||
:label="i18n.baseText('settings.users.invite')"
|
||||
size="large"
|
||||
data-test-id="settings-users-invite-button"
|
||||
@click="onInvite"
|
||||
|
@ -22,15 +239,13 @@
|
|||
<div v-if="!settingsStore.isBelowUserQuota" :class="$style.setupInfoContainer">
|
||||
<n8n-action-box
|
||||
:heading="
|
||||
$locale.baseText(uiStore.contextBasedTranslationKeys.users.settings.unavailable.title)
|
||||
i18n.baseText(uiStore.contextBasedTranslationKeys.users.settings.unavailable.title)
|
||||
"
|
||||
:description="
|
||||
$locale.baseText(
|
||||
uiStore.contextBasedTranslationKeys.users.settings.unavailable.description,
|
||||
)
|
||||
i18n.baseText(uiStore.contextBasedTranslationKeys.users.settings.unavailable.description)
|
||||
"
|
||||
:button-text="
|
||||
$locale.baseText(uiStore.contextBasedTranslationKeys.users.settings.unavailable.button)
|
||||
i18n.baseText(uiStore.contextBasedTranslationKeys.users.settings.unavailable.button)
|
||||
"
|
||||
@click:button="goToUpgrade"
|
||||
/>
|
||||
|
@ -39,7 +254,7 @@
|
|||
<i18n-t keypath="settings.users.advancedPermissions.warning">
|
||||
<template #link>
|
||||
<n8n-link size="small" @click="goToUpgradeAdvancedPermissions">
|
||||
{{ $locale.baseText('settings.users.advancedPermissions.warning.link') }}
|
||||
{{ i18n.baseText('settings.users.advancedPermissions.warning.link') }}
|
||||
</n8n-link>
|
||||
</template>
|
||||
</i18n-t>
|
||||
|
@ -79,237 +294,6 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import { mapStores } from 'pinia';
|
||||
import { EnterpriseEditionFeature, INVITE_USER_MODAL_KEY, VIEWS, ROLE } from '@/constants';
|
||||
|
||||
import type { IRole, IUser, IUserListAction, InvitableRoleName } from '@/Interface';
|
||||
import { useToast } from '@/composables/useToast';
|
||||
import { useUIStore } from '@/stores/ui.store';
|
||||
import { useSettingsStore } from '@/stores/settings.store';
|
||||
import { useUsersStore } from '@/stores/users.store';
|
||||
import { useUsageStore } from '@/stores/usage.store';
|
||||
import { useSSOStore } from '@/stores/sso.store';
|
||||
import { hasPermission } from '@/utils/rbac/permissions';
|
||||
import { useClipboard } from '@/composables/useClipboard';
|
||||
import type { UpdateGlobalRolePayload } from '@/api/users';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'SettingsUsersView',
|
||||
setup() {
|
||||
const clipboard = useClipboard();
|
||||
|
||||
return {
|
||||
clipboard,
|
||||
...useToast(),
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapStores(useSettingsStore, useUIStore, useUsersStore, useUsageStore, useSSOStore),
|
||||
isSharingEnabled() {
|
||||
return this.settingsStore.isEnterpriseFeatureEnabled(EnterpriseEditionFeature.Sharing);
|
||||
},
|
||||
showUMSetupWarning() {
|
||||
return hasPermission(['defaultUser']);
|
||||
},
|
||||
usersListActions(): IUserListAction[] {
|
||||
return [
|
||||
{
|
||||
label: this.$locale.baseText('settings.users.actions.copyInviteLink'),
|
||||
value: 'copyInviteLink',
|
||||
guard: (user) =>
|
||||
this.settingsStore.isBelowUserQuota && !user.firstName && !!user.inviteAcceptUrl,
|
||||
},
|
||||
{
|
||||
label: this.$locale.baseText('settings.users.actions.reinvite'),
|
||||
value: 'reinvite',
|
||||
guard: (user) =>
|
||||
this.settingsStore.isBelowUserQuota &&
|
||||
!user.firstName &&
|
||||
this.settingsStore.isSmtpSetup,
|
||||
},
|
||||
{
|
||||
label: this.$locale.baseText('settings.users.actions.delete'),
|
||||
value: 'delete',
|
||||
guard: (user) =>
|
||||
hasPermission(['rbac'], { rbac: { scope: 'user:delete' } }) &&
|
||||
user.id !== this.usersStore.currentUserId,
|
||||
},
|
||||
{
|
||||
label: this.$locale.baseText('settings.users.actions.copyPasswordResetLink'),
|
||||
value: 'copyPasswordResetLink',
|
||||
guard: (user) =>
|
||||
hasPermission(['rbac'], { rbac: { scope: 'user:resetPassword' } }) &&
|
||||
this.settingsStore.isBelowUserQuota &&
|
||||
!user.isPendingUser &&
|
||||
user.id !== this.usersStore.currentUserId,
|
||||
},
|
||||
{
|
||||
label: this.$locale.baseText('settings.users.actions.allowSSOManualLogin'),
|
||||
value: 'allowSSOManualLogin',
|
||||
guard: (user) =>
|
||||
this.settingsStore.isSamlLoginEnabled && !user.settings?.allowSSOManualLogin,
|
||||
},
|
||||
{
|
||||
label: this.$locale.baseText('settings.users.actions.disallowSSOManualLogin'),
|
||||
value: 'disallowSSOManualLogin',
|
||||
guard: (user) =>
|
||||
this.settingsStore.isSamlLoginEnabled && user.settings?.allowSSOManualLogin === true,
|
||||
},
|
||||
];
|
||||
},
|
||||
isAdvancedPermissionsEnabled(): boolean {
|
||||
return this.settingsStore.isEnterpriseFeatureEnabled(
|
||||
EnterpriseEditionFeature.AdvancedPermissions,
|
||||
);
|
||||
},
|
||||
userRoles(): Array<{ value: IRole; label: string; disabled?: boolean }> {
|
||||
return [
|
||||
{
|
||||
value: ROLE.Member,
|
||||
label: this.$locale.baseText('auth.roles.member'),
|
||||
},
|
||||
{
|
||||
value: ROLE.Admin,
|
||||
label: this.$locale.baseText('auth.roles.admin'),
|
||||
disabled: !this.isAdvancedPermissionsEnabled,
|
||||
},
|
||||
];
|
||||
},
|
||||
canUpdateRole(): boolean {
|
||||
return hasPermission(['rbac'], { rbac: { scope: ['user:update', 'user:changeRole'] } });
|
||||
},
|
||||
},
|
||||
async mounted() {
|
||||
if (!this.showUMSetupWarning) {
|
||||
await this.usersStore.fetchUsers();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async onUsersListAction({ action, userId }: { action: string; userId: string }) {
|
||||
switch (action) {
|
||||
case 'delete':
|
||||
await this.onDelete(userId);
|
||||
break;
|
||||
case 'reinvite':
|
||||
await this.onReinvite(userId);
|
||||
break;
|
||||
case 'copyInviteLink':
|
||||
await this.onCopyInviteLink(userId);
|
||||
break;
|
||||
case 'copyPasswordResetLink':
|
||||
await this.onCopyPasswordResetLink(userId);
|
||||
break;
|
||||
case 'allowSSOManualLogin':
|
||||
await this.onAllowSSOManualLogin(userId);
|
||||
break;
|
||||
case 'disallowSSOManualLogin':
|
||||
await this.onDisallowSSOManualLogin(userId);
|
||||
break;
|
||||
}
|
||||
},
|
||||
redirectToSetup() {
|
||||
void this.$router.push({ name: VIEWS.SETUP });
|
||||
},
|
||||
onInvite() {
|
||||
this.uiStore.openModal(INVITE_USER_MODAL_KEY);
|
||||
},
|
||||
async onDelete(userId: string) {
|
||||
const user = this.usersStore.usersById[userId];
|
||||
if (user) {
|
||||
this.uiStore.openDeleteUserModal(userId);
|
||||
}
|
||||
},
|
||||
async onReinvite(userId: string) {
|
||||
const user = this.usersStore.usersById[userId];
|
||||
if (user?.email && user?.role) {
|
||||
if (!['global:admin', 'global:member'].includes(user.role)) {
|
||||
throw new Error('Invalid role name on reinvite');
|
||||
}
|
||||
try {
|
||||
await this.usersStore.reinviteUser({
|
||||
email: user.email,
|
||||
role: user.role as InvitableRoleName,
|
||||
});
|
||||
this.showToast({
|
||||
type: 'success',
|
||||
title: this.$locale.baseText('settings.users.inviteResent'),
|
||||
message: this.$locale.baseText('settings.users.emailSentTo', {
|
||||
interpolate: { email: user.email ?? '' },
|
||||
}),
|
||||
});
|
||||
} catch (e) {
|
||||
this.showError(e, this.$locale.baseText('settings.users.userReinviteError'));
|
||||
}
|
||||
}
|
||||
},
|
||||
async onCopyInviteLink(userId: string) {
|
||||
const user = this.usersStore.usersById[userId];
|
||||
if (user?.inviteAcceptUrl) {
|
||||
void this.clipboard.copy(user.inviteAcceptUrl);
|
||||
|
||||
this.showToast({
|
||||
type: 'success',
|
||||
title: this.$locale.baseText('settings.users.inviteUrlCreated'),
|
||||
message: this.$locale.baseText('settings.users.inviteUrlCreated.message'),
|
||||
});
|
||||
}
|
||||
},
|
||||
async onCopyPasswordResetLink(userId: string) {
|
||||
const user = this.usersStore.usersById[userId];
|
||||
if (user) {
|
||||
const url = await this.usersStore.getUserPasswordResetLink(user);
|
||||
void this.clipboard.copy(url.link);
|
||||
|
||||
this.showToast({
|
||||
type: 'success',
|
||||
title: this.$locale.baseText('settings.users.passwordResetUrlCreated'),
|
||||
message: this.$locale.baseText('settings.users.passwordResetUrlCreated.message'),
|
||||
});
|
||||
}
|
||||
},
|
||||
async onAllowSSOManualLogin(userId: string) {
|
||||
const user = this.usersStore.usersById[userId];
|
||||
if (user) {
|
||||
if (!user.settings) {
|
||||
user.settings = {};
|
||||
}
|
||||
user.settings.allowSSOManualLogin = true;
|
||||
await this.usersStore.updateOtherUserSettings(userId, user.settings);
|
||||
|
||||
this.showToast({
|
||||
type: 'success',
|
||||
title: this.$locale.baseText('settings.users.allowSSOManualLogin'),
|
||||
message: this.$locale.baseText('settings.users.allowSSOManualLogin.message'),
|
||||
});
|
||||
}
|
||||
},
|
||||
async onDisallowSSOManualLogin(userId: string) {
|
||||
const user = this.usersStore.usersById[userId];
|
||||
if (user?.settings) {
|
||||
user.settings.allowSSOManualLogin = false;
|
||||
await this.usersStore.updateOtherUserSettings(userId, user.settings);
|
||||
this.showToast({
|
||||
type: 'success',
|
||||
title: this.$locale.baseText('settings.users.disallowSSOManualLogin'),
|
||||
message: this.$locale.baseText('settings.users.disallowSSOManualLogin.message'),
|
||||
});
|
||||
}
|
||||
},
|
||||
goToUpgrade() {
|
||||
void this.uiStore.goToUpgrade('settings-users', 'upgrade-users');
|
||||
},
|
||||
goToUpgradeAdvancedPermissions() {
|
||||
void this.uiStore.goToUpgrade('settings-users', 'upgrade-advanced-permissions');
|
||||
},
|
||||
async onRoleChange(user: IUser, newRoleName: UpdateGlobalRolePayload['newRoleName']) {
|
||||
await this.usersStore.updateGlobalRole({ id: user.id, newRoleName });
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.container {
|
||||
height: 100%;
|
||||
|
|
Loading…
Reference in a new issue