mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
feat(editor): Add color selector to sticky node (#7453)
fixes: https://linear.app/n8n/issue/ADO-1223/feature-sticky-colors --------- Co-authored-by: Giulio Andreini <g.andreini@gmail.com> Co-authored-by: Mutasem <mutdmour@gmail.com>
This commit is contained in:
parent
020042ef1a
commit
8359364536
|
@ -1,4 +1,6 @@
|
||||||
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
|
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
|
||||||
|
import { getPopper } from '../utils';
|
||||||
|
import { Interception } from 'cypress/types/net-stubbing';
|
||||||
|
|
||||||
const workflowPage = new WorkflowPageClass();
|
const workflowPage = new WorkflowPageClass();
|
||||||
|
|
||||||
|
@ -66,6 +68,32 @@ describe('Canvas Actions', () => {
|
||||||
workflowPage.getters.stickies().should('have.length', 0);
|
workflowPage.getters.stickies().should('have.length', 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('change sticky color', () => {
|
||||||
|
workflowPage.actions.addSticky();
|
||||||
|
|
||||||
|
workflowPage.getters.stickies().should('have.length', 1);
|
||||||
|
|
||||||
|
workflowPage.actions.toggleColorPalette();
|
||||||
|
|
||||||
|
getPopper().should('be.visible');
|
||||||
|
|
||||||
|
workflowPage.actions.pickColor(2);
|
||||||
|
|
||||||
|
workflowPage.actions.toggleColorPalette();
|
||||||
|
|
||||||
|
getPopper().should('not.be.visible');
|
||||||
|
|
||||||
|
workflowPage.actions.saveWorkflowOnButtonClick();
|
||||||
|
|
||||||
|
cy.wait('@createWorkflow').then((interception: Interception) => {
|
||||||
|
const { request } = interception;
|
||||||
|
const color = request.body?.nodes[0]?.parameters?.color;
|
||||||
|
expect(color).to.equal(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
workflowPage.getters.stickies().should('have.length', 1);
|
||||||
|
});
|
||||||
|
|
||||||
it('edits sticky and updates content as markdown', () => {
|
it('edits sticky and updates content as markdown', () => {
|
||||||
workflowPage.actions.addSticky();
|
workflowPage.actions.addSticky();
|
||||||
|
|
||||||
|
|
|
@ -126,6 +126,7 @@ export class WorkflowPage extends BasePage {
|
||||||
stickies: () => cy.getByTestId('sticky'),
|
stickies: () => cy.getByTestId('sticky'),
|
||||||
editorTabButton: () => cy.getByTestId('radio-button-workflow'),
|
editorTabButton: () => cy.getByTestId('radio-button-workflow'),
|
||||||
workflowHistoryButton: () => cy.getByTestId('workflow-history-button'),
|
workflowHistoryButton: () => cy.getByTestId('workflow-history-button'),
|
||||||
|
colors: () => cy.getByTestId('color'),
|
||||||
};
|
};
|
||||||
actions = {
|
actions = {
|
||||||
visit: (preventNodeViewUnload = true) => {
|
visit: (preventNodeViewUnload = true) => {
|
||||||
|
@ -328,6 +329,17 @@ export class WorkflowPage extends BasePage {
|
||||||
deleteSticky: () => {
|
deleteSticky: () => {
|
||||||
this.getters.stickies().eq(0).realHover().find('[data-test-id="delete-sticky"]').click();
|
this.getters.stickies().eq(0).realHover().find('[data-test-id="delete-sticky"]').click();
|
||||||
},
|
},
|
||||||
|
toggleColorPalette: () => {
|
||||||
|
this.getters
|
||||||
|
.stickies()
|
||||||
|
.eq(0)
|
||||||
|
.realHover()
|
||||||
|
.find('[data-test-id="change-sticky-color"]')
|
||||||
|
.click({ force: true });
|
||||||
|
},
|
||||||
|
pickColor: (index: number) => {
|
||||||
|
this.getters.colors().eq(1).click();
|
||||||
|
},
|
||||||
editSticky: (content: string) => {
|
editSticky: (content: string) => {
|
||||||
this.getters.stickies().dblclick().find('textarea').clear().type(content).type('{esc}');
|
this.getters.stickies().dblclick().find('textarea').clear().type(content).type('{esc}');
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,6 +1,11 @@
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
:class="{ 'n8n-sticky': true, [$style.sticky]: true, [$style.clickable]: !isResizing }"
|
:class="{
|
||||||
|
'n8n-sticky': true,
|
||||||
|
[$style.sticky]: true,
|
||||||
|
[$style.clickable]: !isResizing,
|
||||||
|
[$style[`color-${backgroundColor}`]]: true,
|
||||||
|
}"
|
||||||
:style="styles"
|
:style="styles"
|
||||||
@keydown.prevent
|
@keydown.prevent
|
||||||
>
|
>
|
||||||
|
@ -106,6 +111,10 @@ export default defineComponent({
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
backgroundColor: {
|
||||||
|
value: [Number, String],
|
||||||
|
default: 1,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
N8nInput,
|
N8nInput,
|
||||||
|
@ -132,10 +141,12 @@ export default defineComponent({
|
||||||
return this.width;
|
return this.width;
|
||||||
},
|
},
|
||||||
styles(): { height: string; width: string } {
|
styles(): { height: string; width: string } {
|
||||||
return {
|
const styles: { height: string; width: string } = {
|
||||||
height: `${this.resHeight}px`,
|
height: `${this.resHeight}px`,
|
||||||
width: `${this.resWidth}px`,
|
width: `${this.resWidth}px`,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
return styles;
|
||||||
},
|
},
|
||||||
shouldShowFooter(): boolean {
|
shouldShowFooter(): boolean {
|
||||||
return this.resHeight > 100 && this.resWidth > 155;
|
return this.resHeight > 100 && this.resWidth > 155;
|
||||||
|
@ -189,8 +200,6 @@ export default defineComponent({
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
.sticky {
|
.sticky {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
background-color: var(--color-sticky-default-background);
|
|
||||||
border: 1px solid var(--color-sticky-default-border);
|
|
||||||
border-radius: var(--border-radius-base);
|
border-radius: var(--border-radius-base);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -212,12 +221,6 @@ export default defineComponent({
|
||||||
left: 0;
|
left: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
background: linear-gradient(
|
|
||||||
180deg,
|
|
||||||
var(--color-sticky-default-background),
|
|
||||||
#fff5d600 0.01%,
|
|
||||||
var(--color-sticky-default-background)
|
|
||||||
);
|
|
||||||
border-radius: var(--border-radius-base);
|
border-radius: var(--border-radius-base);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -227,6 +230,100 @@ export default defineComponent({
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.color-1 {
|
||||||
|
background-color: var(--sticky-color-1);
|
||||||
|
border: 1px solid var(--color-sticky, var(--sticky-color-1));
|
||||||
|
|
||||||
|
.wrapper::after {
|
||||||
|
background: linear-gradient(
|
||||||
|
180deg,
|
||||||
|
var(--color-sticky, var(--sticky-color-1)),
|
||||||
|
#fff5d600 0.01%,
|
||||||
|
var(--color-sticky, var(--sticky-color-1))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-2 {
|
||||||
|
background-color: var(--sticky-color-2);
|
||||||
|
border: 1px solid var(--color-sticky, var(--sticky-color-2));
|
||||||
|
.wrapper::after {
|
||||||
|
background: linear-gradient(
|
||||||
|
180deg,
|
||||||
|
var(--color-sticky, var(--sticky-color-2)),
|
||||||
|
#fff5d600 0.01%,
|
||||||
|
var(--color-sticky, var(--sticky-color-2))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-3 {
|
||||||
|
background-color: var(--sticky-color-3);
|
||||||
|
border: 1px solid var(--color-sticky, var(--sticky-color-3));
|
||||||
|
.wrapper::after {
|
||||||
|
background: linear-gradient(
|
||||||
|
180deg,
|
||||||
|
var(--color-sticky, var(--sticky-color-3)),
|
||||||
|
#fff5d600 0.01%,
|
||||||
|
var(--color-sticky, var(--sticky-color-3))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-4 {
|
||||||
|
background-color: var(--sticky-color-4);
|
||||||
|
border: 1px solid var(--color-sticky, var(--sticky-color-4));
|
||||||
|
.wrapper::after {
|
||||||
|
background: linear-gradient(
|
||||||
|
180deg,
|
||||||
|
var(--color-sticky, var(--sticky-color-4)),
|
||||||
|
#fff5d600 0.01%,
|
||||||
|
var(--color-sticky, var(--sticky-color-4))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-5 {
|
||||||
|
background-color: var(--sticky-color-5);
|
||||||
|
border: 1px solid var(--color-sticky, var(--sticky-color-5));
|
||||||
|
.wrapper::after {
|
||||||
|
background: linear-gradient(
|
||||||
|
180deg,
|
||||||
|
var(--color-sticky, var(--sticky-color-5)),
|
||||||
|
#fff5d600 0.01%,
|
||||||
|
var(--color-sticky, var(--sticky-color-5))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-6 {
|
||||||
|
background-color: var(--sticky-color-6);
|
||||||
|
border: 1px solid var(--color-sticky, var(--sticky-color-6));
|
||||||
|
|
||||||
|
.wrapper::after {
|
||||||
|
background: linear-gradient(
|
||||||
|
180deg,
|
||||||
|
var(--color-sticky, var(--sticky-color-6)),
|
||||||
|
#fff5d600 0.01%,
|
||||||
|
var(--color-sticky, var(--sticky-color-6))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-7 {
|
||||||
|
background-color: var(--sticky-color-7);
|
||||||
|
border: 1px solid var(--color-sticky, var(--sticky-color-7));
|
||||||
|
|
||||||
|
.wrapper::after {
|
||||||
|
background: linear-gradient(
|
||||||
|
180deg,
|
||||||
|
var(--color-sticky, var(--sticky-color-7)),
|
||||||
|
#fff5d600 0.01%,
|
||||||
|
var(--color-sticky, var(--sticky-color-7))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
|
|
@ -79,6 +79,14 @@
|
||||||
--color-sticky-font: var(--prim-gray-740);
|
--color-sticky-font: var(--prim-gray-740);
|
||||||
--color-sticky-code-background: var(--color-background-base);
|
--color-sticky-code-background: var(--color-background-base);
|
||||||
|
|
||||||
|
--sticky-color-7: #f0f3f9;
|
||||||
|
--sticky-color-6: #e7d6ff;
|
||||||
|
--sticky-color-5: #d6ebff;
|
||||||
|
--sticky-color-4: #dcf9eb;
|
||||||
|
--sticky-color-3: #fbdadd;
|
||||||
|
--sticky-color-2: #fde9d8;
|
||||||
|
--sticky-color-1: #fff5d6;
|
||||||
|
|
||||||
// Expressions
|
// Expressions
|
||||||
--color-valid-resolvable-foreground: var(--prim-color-alt-a);
|
--color-valid-resolvable-foreground: var(--prim-color-alt-a);
|
||||||
--color-valid-resolvable-background: var(--prim-color-alt-a-tint-500);
|
--color-valid-resolvable-background: var(--prim-color-alt-a-tint-500);
|
||||||
|
|
|
@ -27,6 +27,7 @@
|
||||||
:height="node.parameters.height"
|
:height="node.parameters.height"
|
||||||
:width="node.parameters.width"
|
:width="node.parameters.width"
|
||||||
:scale="nodeViewScale"
|
:scale="nodeViewScale"
|
||||||
|
:backgroundColor="node.parameters.color"
|
||||||
:id="node.id"
|
:id="node.id"
|
||||||
:readOnly="isReadOnly"
|
:readOnly="isReadOnly"
|
||||||
:defaultText="defaultText"
|
:defaultText="defaultText"
|
||||||
|
@ -41,7 +42,10 @@
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-show="showActions" class="sticky-options no-select-on-click">
|
<div
|
||||||
|
v-show="showActions"
|
||||||
|
:class="{ 'sticky-options': true, 'no-select-on-click': true, 'force-show': forceActions }"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
v-touch:tap="deleteNode"
|
v-touch:tap="deleteNode"
|
||||||
class="option"
|
class="option"
|
||||||
|
@ -50,6 +54,45 @@
|
||||||
>
|
>
|
||||||
<font-awesome-icon icon="trash" />
|
<font-awesome-icon icon="trash" />
|
||||||
</div>
|
</div>
|
||||||
|
<n8n-popover
|
||||||
|
effect="dark"
|
||||||
|
:popper-style="{ width: '208px' }"
|
||||||
|
trigger="click"
|
||||||
|
placement="top"
|
||||||
|
@show="onShowPopover"
|
||||||
|
@hide="onHidePopover"
|
||||||
|
>
|
||||||
|
<template #reference>
|
||||||
|
<div
|
||||||
|
class="option"
|
||||||
|
data-test-id="change-sticky-color"
|
||||||
|
:title="$locale.baseText('node.changeColor')"
|
||||||
|
>
|
||||||
|
<font-awesome-icon icon="palette" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div class="content">
|
||||||
|
<div
|
||||||
|
class="color"
|
||||||
|
data-test-id="color"
|
||||||
|
v-for="(_, index) in Array.from({ length: 7 })"
|
||||||
|
:key="index"
|
||||||
|
v-on:click="changeColor(index + 1)"
|
||||||
|
:class="`sticky-color-${index + 1}`"
|
||||||
|
:style="{
|
||||||
|
'border-width': '1px',
|
||||||
|
'border-style': 'solid',
|
||||||
|
'border-color': 'var(--color-text-dark)',
|
||||||
|
'background-color': `var(--sticky-color-${index + 1})`,
|
||||||
|
'box-shadow':
|
||||||
|
(index === 0 && node?.parameters.color === '') ||
|
||||||
|
index + 1 === node?.parameters.color
|
||||||
|
? `0 0 0 1px var(--sticky-color-${index + 1})`
|
||||||
|
: 'none',
|
||||||
|
}"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</n8n-popover>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -149,7 +192,10 @@ export default defineComponent({
|
||||||
return returnStyles;
|
return returnStyles;
|
||||||
},
|
},
|
||||||
showActions(): boolean {
|
showActions(): boolean {
|
||||||
return !(this.hideActions || this.isReadOnly || this.workflowRunning || this.isResizing);
|
return (
|
||||||
|
!(this.hideActions || this.isReadOnly || this.workflowRunning || this.isResizing) ||
|
||||||
|
this.forceActions
|
||||||
|
);
|
||||||
},
|
},
|
||||||
workflowRunning(): boolean {
|
workflowRunning(): boolean {
|
||||||
return this.uiStore.isActionActive('workflowRunning');
|
return this.uiStore.isActionActive('workflowRunning');
|
||||||
|
@ -157,16 +203,29 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
forceActions: false,
|
||||||
isResizing: false,
|
isResizing: false,
|
||||||
isTouchActive: false,
|
isTouchActive: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
onShowPopover() {
|
||||||
|
this.forceActions = true;
|
||||||
|
},
|
||||||
|
onHidePopover() {
|
||||||
|
this.forceActions = false;
|
||||||
|
},
|
||||||
async deleteNode() {
|
async deleteNode() {
|
||||||
// Wait a tick else vue causes problems because the data is gone
|
// Wait a tick else vue causes problems because the data is gone
|
||||||
await this.$nextTick();
|
await this.$nextTick();
|
||||||
this.$emit('removeNode', this.data.name);
|
this.$emit('removeNode', this.data.name);
|
||||||
},
|
},
|
||||||
|
changeColor(index: number) {
|
||||||
|
this.workflowsStore.updateNodeProperties({
|
||||||
|
name: this.name,
|
||||||
|
properties: { parameters: { ...this.node.parameters, color: index } },
|
||||||
|
});
|
||||||
|
},
|
||||||
onEdit(edit: boolean) {
|
onEdit(edit: boolean) {
|
||||||
if (edit && !this.isActive && this.node) {
|
if (edit && !this.isActive && this.node) {
|
||||||
this.ndvStore.activeNodeName = this.node.name;
|
this.ndvStore.activeNodeName = this.node.name;
|
||||||
|
@ -211,12 +270,13 @@ export default defineComponent({
|
||||||
onResizeEnd() {
|
onResizeEnd() {
|
||||||
this.isResizing = false;
|
this.isResizing = false;
|
||||||
},
|
},
|
||||||
setParameters(params: { content?: string; height?: number; width?: number }) {
|
setParameters(params: { content?: string; height?: number; width?: number; color?: string }) {
|
||||||
if (this.node) {
|
if (this.node) {
|
||||||
const nodeParameters = {
|
const nodeParameters = {
|
||||||
content: isString(params.content) ? params.content : this.node.parameters.content,
|
content: isString(params.content) ? params.content : this.node.parameters.content,
|
||||||
height: isNumber(params.height) ? params.height : this.node.parameters.height,
|
height: isNumber(params.height) ? params.height : this.node.parameters.height,
|
||||||
width: isNumber(params.width) ? params.width : this.node.parameters.width,
|
width: isNumber(params.width) ? params.width : this.node.parameters.width,
|
||||||
|
color: isString(params.color) ? params.color : this.node.parameters.color,
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateInformation: IUpdateInformation = {
|
const updateInformation: IUpdateInformation = {
|
||||||
|
@ -299,6 +359,10 @@ export default defineComponent({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.force-show {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
&.is-touch-device .sticky-options {
|
&.is-touch-device .sticky-options {
|
||||||
left: -25px;
|
left: -25px;
|
||||||
width: 150px;
|
width: 150px;
|
||||||
|
@ -322,4 +386,22 @@ export default defineComponent({
|
||||||
top: -8px;
|
top: -8px;
|
||||||
z-index: 0;
|
z-index: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
width: fit-content;
|
||||||
|
gap: var(--spacing-2xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.color {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border-color: var(--color-primary-shade-1);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -805,6 +805,7 @@
|
||||||
"node.thisIsATriggerNode": "This is a Trigger node. <a target=\"_blank\" href=\"https://docs.n8n.io/workflows/components/nodes/\">Learn more</a>",
|
"node.thisIsATriggerNode": "This is a Trigger node. <a target=\"_blank\" href=\"https://docs.n8n.io/workflows/components/nodes/\">Learn more</a>",
|
||||||
"node.activateDeactivateNode": "Activate/Deactivate Node",
|
"node.activateDeactivateNode": "Activate/Deactivate Node",
|
||||||
"node.deleteNode": "Delete Node",
|
"node.deleteNode": "Delete Node",
|
||||||
|
"node.changeColor": "Change Color",
|
||||||
"node.disabled": "Disabled",
|
"node.disabled": "Disabled",
|
||||||
"node.duplicateNode": "Duplicate Node",
|
"node.duplicateNode": "Duplicate Node",
|
||||||
"node.editNode": "Edit Node",
|
"node.editNode": "Edit Node",
|
||||||
|
|
|
@ -94,6 +94,7 @@ import {
|
||||||
faMapSigns,
|
faMapSigns,
|
||||||
faMousePointer,
|
faMousePointer,
|
||||||
faNetworkWired,
|
faNetworkWired,
|
||||||
|
faPalette,
|
||||||
faPause,
|
faPause,
|
||||||
faPauseCircle,
|
faPauseCircle,
|
||||||
faPen,
|
faPen,
|
||||||
|
@ -253,6 +254,7 @@ export const FontAwesomePlugin: Plugin<{}> = {
|
||||||
addIcon(faMapSigns);
|
addIcon(faMapSigns);
|
||||||
addIcon(faMousePointer);
|
addIcon(faMousePointer);
|
||||||
addIcon(faNetworkWired);
|
addIcon(faNetworkWired);
|
||||||
|
addIcon(faPalette);
|
||||||
addIcon(faPause);
|
addIcon(faPause);
|
||||||
addIcon(faPauseCircle);
|
addIcon(faPauseCircle);
|
||||||
addIcon(faPen);
|
addIcon(faPen);
|
||||||
|
|
|
@ -44,6 +44,14 @@ export class StickyNote implements INodeType {
|
||||||
required: true,
|
required: true,
|
||||||
default: 240,
|
default: 240,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Color',
|
||||||
|
name: 'color',
|
||||||
|
// eslint-disable-next-line n8n-nodes-base/node-param-color-type-unused
|
||||||
|
type: 'number',
|
||||||
|
required: true,
|
||||||
|
default: 1,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue