mirror of
https://github.com/n8n-io/n8n.git
synced 2024-11-09 17:50:52 +03: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 { getPopper } from '../utils';
|
||||
import { Interception } from 'cypress/types/net-stubbing';
|
||||
|
||||
const workflowPage = new WorkflowPageClass();
|
||||
|
||||
@ -66,6 +68,32 @@ describe('Canvas Actions', () => {
|
||||
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', () => {
|
||||
workflowPage.actions.addSticky();
|
||||
|
||||
|
@ -126,6 +126,7 @@ export class WorkflowPage extends BasePage {
|
||||
stickies: () => cy.getByTestId('sticky'),
|
||||
editorTabButton: () => cy.getByTestId('radio-button-workflow'),
|
||||
workflowHistoryButton: () => cy.getByTestId('workflow-history-button'),
|
||||
colors: () => cy.getByTestId('color'),
|
||||
};
|
||||
actions = {
|
||||
visit: (preventNodeViewUnload = true) => {
|
||||
@ -328,6 +329,17 @@ export class WorkflowPage extends BasePage {
|
||||
deleteSticky: () => {
|
||||
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) => {
|
||||
this.getters.stickies().dblclick().find('textarea').clear().type(content).type('{esc}');
|
||||
},
|
||||
|
@ -1,6 +1,11 @@
|
||||
<template>
|
||||
<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"
|
||||
@keydown.prevent
|
||||
>
|
||||
@ -106,6 +111,10 @@ export default defineComponent({
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
backgroundColor: {
|
||||
value: [Number, String],
|
||||
default: 1,
|
||||
},
|
||||
},
|
||||
components: {
|
||||
N8nInput,
|
||||
@ -132,10 +141,12 @@ export default defineComponent({
|
||||
return this.width;
|
||||
},
|
||||
styles(): { height: string; width: string } {
|
||||
return {
|
||||
const styles: { height: string; width: string } = {
|
||||
height: `${this.resHeight}px`,
|
||||
width: `${this.resWidth}px`,
|
||||
};
|
||||
|
||||
return styles;
|
||||
},
|
||||
shouldShowFooter(): boolean {
|
||||
return this.resHeight > 100 && this.resWidth > 155;
|
||||
@ -189,8 +200,6 @@ export default defineComponent({
|
||||
<style lang="scss" module>
|
||||
.sticky {
|
||||
position: absolute;
|
||||
background-color: var(--color-sticky-default-background);
|
||||
border: 1px solid var(--color-sticky-default-border);
|
||||
border-radius: var(--border-radius-base);
|
||||
}
|
||||
|
||||
@ -212,12 +221,6 @@ export default defineComponent({
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
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);
|
||||
}
|
||||
}
|
||||
@ -227,6 +230,100 @@ export default defineComponent({
|
||||
display: flex;
|
||||
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 lang="scss">
|
||||
|
@ -79,6 +79,14 @@
|
||||
--color-sticky-font: var(--prim-gray-740);
|
||||
--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
|
||||
--color-valid-resolvable-foreground: var(--prim-color-alt-a);
|
||||
--color-valid-resolvable-background: var(--prim-color-alt-a-tint-500);
|
||||
|
@ -27,6 +27,7 @@
|
||||
:height="node.parameters.height"
|
||||
:width="node.parameters.width"
|
||||
:scale="nodeViewScale"
|
||||
:backgroundColor="node.parameters.color"
|
||||
:id="node.id"
|
||||
:readOnly="isReadOnly"
|
||||
:defaultText="defaultText"
|
||||
@ -41,7 +42,10 @@
|
||||
/>
|
||||
</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
|
||||
v-touch:tap="deleteNode"
|
||||
class="option"
|
||||
@ -50,6 +54,45 @@
|
||||
>
|
||||
<font-awesome-icon icon="trash" />
|
||||
</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>
|
||||
@ -149,7 +192,10 @@ export default defineComponent({
|
||||
return returnStyles;
|
||||
},
|
||||
showActions(): boolean {
|
||||
return !(this.hideActions || this.isReadOnly || this.workflowRunning || this.isResizing);
|
||||
return (
|
||||
!(this.hideActions || this.isReadOnly || this.workflowRunning || this.isResizing) ||
|
||||
this.forceActions
|
||||
);
|
||||
},
|
||||
workflowRunning(): boolean {
|
||||
return this.uiStore.isActionActive('workflowRunning');
|
||||
@ -157,16 +203,29 @@ export default defineComponent({
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
forceActions: false,
|
||||
isResizing: false,
|
||||
isTouchActive: false,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
onShowPopover() {
|
||||
this.forceActions = true;
|
||||
},
|
||||
onHidePopover() {
|
||||
this.forceActions = false;
|
||||
},
|
||||
async deleteNode() {
|
||||
// Wait a tick else vue causes problems because the data is gone
|
||||
await this.$nextTick();
|
||||
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) {
|
||||
if (edit && !this.isActive && this.node) {
|
||||
this.ndvStore.activeNodeName = this.node.name;
|
||||
@ -211,12 +270,13 @@ export default defineComponent({
|
||||
onResizeEnd() {
|
||||
this.isResizing = false;
|
||||
},
|
||||
setParameters(params: { content?: string; height?: number; width?: number }) {
|
||||
setParameters(params: { content?: string; height?: number; width?: number; color?: string }) {
|
||||
if (this.node) {
|
||||
const nodeParameters = {
|
||||
content: isString(params.content) ? params.content : this.node.parameters.content,
|
||||
height: isNumber(params.height) ? params.height : this.node.parameters.height,
|
||||
width: isNumber(params.width) ? params.width : this.node.parameters.width,
|
||||
color: isString(params.color) ? params.color : this.node.parameters.color,
|
||||
};
|
||||
|
||||
const updateInformation: IUpdateInformation = {
|
||||
@ -299,6 +359,10 @@ export default defineComponent({
|
||||
}
|
||||
}
|
||||
|
||||
.force-show {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
&.is-touch-device .sticky-options {
|
||||
left: -25px;
|
||||
width: 150px;
|
||||
@ -322,4 +386,22 @@ export default defineComponent({
|
||||
top: -8px;
|
||||
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>
|
||||
|
@ -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.activateDeactivateNode": "Activate/Deactivate Node",
|
||||
"node.deleteNode": "Delete Node",
|
||||
"node.changeColor": "Change Color",
|
||||
"node.disabled": "Disabled",
|
||||
"node.duplicateNode": "Duplicate Node",
|
||||
"node.editNode": "Edit Node",
|
||||
|
@ -94,6 +94,7 @@ import {
|
||||
faMapSigns,
|
||||
faMousePointer,
|
||||
faNetworkWired,
|
||||
faPalette,
|
||||
faPause,
|
||||
faPauseCircle,
|
||||
faPen,
|
||||
@ -253,6 +254,7 @@ export const FontAwesomePlugin: Plugin<{}> = {
|
||||
addIcon(faMapSigns);
|
||||
addIcon(faMousePointer);
|
||||
addIcon(faNetworkWired);
|
||||
addIcon(faPalette);
|
||||
addIcon(faPause);
|
||||
addIcon(faPauseCircle);
|
||||
addIcon(faPen);
|
||||
|
@ -44,6 +44,14 @@ export class StickyNote implements INodeType {
|
||||
required: true,
|
||||
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 New Issue
Block a user