Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FOUR-13453 order calcs list with drag and drop option #1612

Merged
merged 8 commits into from
Jun 12, 2024
Merged
174 changes: 95 additions & 79 deletions src/components/computed-properties.vue
Original file line number Diff line number Diff line change
@@ -15,79 +15,73 @@
@hidden="displayTableList"
>
<template v-if="displayList">
<div class="d-flex align-items-end flex-column mb-3">
<button
type="button"
class="btn btn-secondary"
data-cy="calcs-add-property"
@click.stop="displayFormProperty"
>
<i class="fas fa-plus" /> {{ $t("Property") }}
</button>
</div>
<div class="card card-body table-card">
<b-table
:css="css"
:fields="fields"
:items="current"
:empty-text="$t('No Data Available')"
data-cy="calcs-table"
>
<template #cell(__actions)="{ item }">
<div class="actions">
<div class="popout">
<b-btn
v-b-tooltip.hover
variant="link"
:title="$t('Edit')"
data-cy="calcs-table-edit"
@click="editProperty(item)"
>
<i class="fas fa-edit fa-lg fa-fw" />
</b-btn>
<b-btn
v-b-tooltip.hover
variant="link"
:title="$t('Delete')"
data-cy="calcs-table-remove"
@click="deleteProperty(item)"
>
<i class="fas fa-trash-alt fa-lg fa-fw" />
</b-btn>
</div>
</div>
</template>
</b-table>
</div>
<Sortable
:fields="fields"
:items="current"
filter-key="name"
disable-key="byPass"
:inline-edit="false"
:data-test-actions="{
tableBox: { 'data-cy': 'calcs-table' },
btnNew: { 'data-cy': 'calcs-add-property' },
btnEdit: { 'data-cy': 'calcs-table-edit' },
btnDelete: { 'data-cy': 'calcs-table-remove' },
}"
@item-edit="editProperty"
@item-delete="deleteProperty"
@add-page="displayFormProperty"
>
<template #options="{ item }">
<button
v-b-tooltip="{ customClass: 'bypass-btn-tooltip' }"
:title="item.byPass ? $t('Unbypass Calc') : $t('Bypass Calc')"
class="btn"
data-test="calcs-bypass"
@click="toggleBypass(item.id)"
>
<i v-show="!item.byPass" class="fas fa-sign-out-alt"></i>
<i v-show="item.byPass" class="fas fa-sign-in-alt"></i>
</button>
<div class="sortable-item-vr"></div>
</template>
</Sortable>

<template slot="modal-footer">
<span />
</template>
</template>

<template v-else>
<required />
<form-input
ref="property"
v-model="add.property"
:label="$t('Property Name') + ' *'"
name="property"
:error="errors.property"
class="mb-3"
data-cy="calcs-property-name"
required
aria-required="true"
/>
<form-text-area
ref="name"
v-model="add.name"
:label="$t('Description') + ' *'"
name="name"
:error="errors.name"
class="mb-3"
data-cy="calcs-property-description"
required
aria-required="true"
/>
<b-container>
<b-row class="p-0">
<b-col class="pl-0">
<form-input
ref="property"
v-model="add.property"
:label="$t('Property Name') + ' *'"
name="property"
:error="errors.property"
class="mb-3"
data-cy="calcs-property-name"
required
aria-required="true"
/>
</b-col>
<b-col class="pr-0">
<form-text-area
ref="name"
v-model="add.name"
:label="$t('Description') + ' *'"
name="name"
:error="errors.name"
class="mb-3"
data-cy="calcs-property-description"
required
aria-required="true"
/>
</b-col>
</b-row>
</b-container>
<div class="form-group mb-3" style="position: relative">
<label v-show="isJS">{{ $t("Formula") + " *" }}</label>
<div class="float-right">
@@ -168,12 +162,14 @@ import { FormInput, FormTextArea } from "@processmaker/vue-form-elements";
import MonacoEditor from "vue-monaco";
import Validator from "@chantouchsek/validatorjs";
import FocusErrors from "../mixins/focusErrors";
import Sortable from './sortable/Sortable.vue';

export default {
components: {
FormInput,
FormTextArea,
MonacoEditor
MonacoEditor,
Sortable,
},
mixins: [FocusErrors],
props: ["value"],
@@ -208,18 +204,13 @@ export default {
},
fields: [
{
label: this.$t("Property Name"),
key: "property"
label: this.$t("Name"),
key: "property",
},
{
label: this.$t("Description"),
key: "name"
label: this.$t("Type"),
key: "type",
},
{
key: "__actions",
label: "",
class: "text-right"
}
],
monacoOptions: {
automaticLayout: true,
@@ -258,9 +249,14 @@ export default {
this.value.forEach((item) => {
this.numberItem++;
item.id = this.numberItem;

if (!Object.hasOwn(item, 'byPass')) {
item.byPass = false;
}
});

this.current = this.value;
}
},
},
created() {
Validator.register(
@@ -334,6 +330,13 @@ export default {
this.focusFirstCalculatedPropertiesError();
}
},
toggleBypass(itemId) {
this.current = this.current.map((item) =>
item.id === itemId ? { ...item, byPass: !item.byPass } : item,
);

this.$emit("input", this.current);
},
saveProperty() {
if (this.add.id === 0) {
this.numberItem++;
@@ -342,7 +345,8 @@ export default {
property: this.add.property,
name: this.add.name,
formula: this.add.formula,
type: this.add.type
type: this.add.type,
byPass: false,
});
} else {
this.current.forEach((item) => {
@@ -400,4 +404,16 @@ export default {
.editor-border.is-invalid {
border-color: #dc3545;
}

.bypass-btn-tooltip::v-deep {
& .tooltip-inner {
background-color: #EBEEF2 !important;
color: #444444 !important;
}

& .arrow:before {
border-top-color: #EBEEF2 !important;
border-bottom-color: #EBEEF2 !important;
}
}
</style>
29 changes: 25 additions & 4 deletions src/components/sortable/Sortable.vue
Original file line number Diff line number Diff line change
@@ -15,6 +15,7 @@
<button
type="button"
class="btn sortable-btn-new"
v-bind="dataTestActions.btnNew"
@click="$emit('add-page', $event)"
>
<i class="fa fa-plus"></i>
@@ -23,26 +24,46 @@
</div>

<SortableList
:fields="fields"
:items="items"
:filtered-items="filteredItems"
:inline-edit="inlineEdit"
:disable-key="disableKey"
:data-test-actions="dataTestActions"
@ordered="$emit('ordered', $event)"
@item-edit="$emit('item-edit', $event)"
@item-delete="$emit('item-delete', $event)"
/>
>
<template #options="{ item }">
<slot name="options" :item="item"></slot>
</template>
</SortableList>
</div>
</template>

<script>
import SortableList from './sortableList/SortableList.vue'
import SortableList from './sortableList/SortableList.vue';

export default {
name: 'Sortable',
components: {
SortableList
SortableList,
},
props: {
fields: { type: Array, required: true },
items: { type: Array, required: true },
filterKey: { type: String, required: true },
disableKey: { type: String, default: null },
inlineEdit: { type: Boolean, default: true },
dataTestActions: {
type: Object,
default: () => ({
tableBox: { 'data-test': 'sortable-table-box' },
btnNew: { 'data-test': 'sortable-btn-new' },
btnEdit: { 'data-test': 'sortable-btn-edit' },
btnDelete: { 'data-test': 'sortable-btn-remove' },
}),
},
},
data() {
return {
@@ -54,7 +75,7 @@ export default {
this.$set(item, "order", index + 1);
}
return item;
})
}),
};
},
watch: {
116 changes: 81 additions & 35 deletions src/components/sortable/sortableList/SortableList.vue
Original file line number Diff line number Diff line change
@@ -1,51 +1,85 @@
<template>
<div class="row mt-3">
<div class="col p-0 border rounded-lg sortable-list">
<div class="sortable-list-header">
<div class="sortable-item-icon"></div>
<div class="sortable-list-title">PAGE NAME</div>
</div>
<div class="sortable-container" @dragover="dragOver">
<div
class="p-0 border rounded-lg sortable-list"
v-bind="dataTestActions.tableBox"
@dragover="dragOver"
>
<div class="sortable-list-tr">
<div class="sortable-list-td"></div>
<div
v-for="(item, index) in sortedItems"
:key="index"
:data-order="item.order"
:data-test="`item-${item.order}`"
:title="item.name"
draggable="true"
@dragstart="(event) => dragStart(event, item.order)"
@dragenter="(event) => dragEnter(event, item.order)"
@dragend="dragEnd"
class="sortable-item sortable-draggable"
v-for="field in fields"
:key="field.key"
class="sortable-list-td sortable-list-header"
>
{{ field.label }}
</div>
<div class="sortable-list-td"></div>
</div>
<div
v-for="(item, index) in sortedItems"
:key="`item-${index}`"
:data-order="item.order"
:data-test="`item-${item.order}`"
:title="item.name"
:class="[
'sortable-list-tr',
'sortable-item',
{ 'sortable-item-disabled': isDisabled(item) },
]"
draggable="true"
@dragstart="(event) => dragStart(event, item.order)"
@dragenter="(event) => dragEnter(event, item.order)"
@dragend="dragEnd"
>
<div class="sortable-list-td">
<div class="sortable-item-icon">
<i class="fas fa-bars"></i>
</div>
<div class="rounded sortable-item-name">
<b-form-input
v-if="editRowIndex === index"
v-model="newName"
type="text"
autofocus
required
:state="validateState(newName, item)"
:error="validateError(newName, item)"
@blur.stop="onBlur(newName, item)"
@keydown.enter.stop="onBlur(newName, item)"
@keydown.esc.stop="onCancel(item)"
@focus="onFocus(item)"
/>
<span v-else>{{ item.name }}</span>
</div>
</div>
<div
v-for="field in fields"
:key="field.key"
class="sortable-list-td sortable-item-prop"
>
<b-form-input
v-if="editRowIndex === index"
v-model="newName"
type="text"
autofocus
required
:state="validateState(newName, item)"
:error="validateError(newName, item)"
@blur.stop="onBlur(newName, item)"
@keydown.enter.stop="onBlur(newName, item)"
@keydown.esc.stop="onCancel(item)"
@focus="onFocus(item)"
/>
<span v-else>{{ item[field.key] }}</span>
</div>

<div class="sortable-list-td">
<div class="border rounded-lg sortable-item-action">
<button v-if="editRowIndex === index" class="btn">
<i class="fas fa-check"></i>
</button>
<button v-else class="btn" @click.stop="onClick(item, index)">
<button
v-else
class="btn"
title="Edit"
v-bind="dataTestActions.btnEdit"
@click.stop="onClick(item, index)"
>
<i class="fas fa-edit"></i>
</button>
<div class="sortable-item-vr"></div>
<button class="btn" @click="$emit('item-delete', item)">
<slot name="options" :item="item"></slot>
<button
class="btn"
title="Delete"
v-bind="dataTestActions.btnDelete"
@click="$emit('item-delete', item)"
>
<i class="fas fa-trash-alt"></i>
</button>
</div>
@@ -59,8 +93,12 @@
export default {
name: 'SortableList',
props: {
fields: { type: Array, required: true },
items: { type: Array, required: true },
filteredItems: { type: Array, required: true },
inlineEdit: { type: Boolean, default: true },
disableKey: { type: String, default: null },
dataTestActions: { type: Object, required: true },
},
data() {
return {
@@ -77,7 +115,7 @@ export default {
this.refreshSort &&
[...this.filteredItems].sort((a, b) => a.order - b.order);
return sortedItems;
}
},
},
methods: {
validateState(name, item) {
@@ -120,6 +158,11 @@ export default {
this.editRowIndex = null;
},
onClick(item, index) {
if (!this.inlineEdit) {
this.$emit("item-edit", item);
return;
}
this.editRowIndex = index;
this.$emit("item-edit", item);
},
@@ -185,6 +228,9 @@ export default {
dragOver(event) {
event.preventDefault();
},
isDisabled(item) {
return this.disableKey ? item[this.disableKey] : false;
},
},
}
</script>
60 changes: 38 additions & 22 deletions src/components/sortable/sortableList/sortableList.scss
Original file line number Diff line number Diff line change
@@ -2,53 +2,65 @@ $border-color: #cdddee;

.sortable {
&-list {
display: flex;
display: table;
flex-direction: column;
width: 100%;
border: 1px solid $border-color !important;

&-header {
display: flex;
align-items: center;
&-tr {
display: table-row;

&:last-child > .sortable-list-td {
border-bottom: none;
}
}

&-td {
display: table-cell;
border-bottom: 1px solid $border-color;

&:first-child {
width: 9%;
}

&:nth-child(2) {
width: 40%;
}

&:not(:first-child):not(:nth-child(2)):not(:last-child) {
width: auto;
}

&:last-child {
width: 1%;
white-space: nowrap;
}
}

&-title {
padding-left: 16px;
&-header {
padding: 16px 0 16px 16px;
font-size: 14px;
font-weight: bold;
font-weight: 700;
color: #566877;
text-transform: uppercase;
}
}

&-container {
display: flex;
flex-direction: column;
width: 100%;
height: 340px;
overflow-x: auto;
}

&-item {
display: flex;
align-items: center;
height: 56px;
border-bottom: 1px solid $border-color;
cursor: move;

&-icon {
display: flex;
justify-content: center;
align-items: center;
width: 64px;
height: 56px;
}

& .fas {
color: #6A7888;
}

&-name {
flex-grow: 1;
&-prop {
padding: 8px 16px;
font-size: 15px;
color: #556271;
@@ -65,6 +77,10 @@ $border-color: #cdddee;
margin: 9px 0;
border-right: 1px solid $border-color;
}

&-disabled > .sortable-item-prop {
color: rgba(85, 98, 113, 0.5);
}
}
}

24 changes: 16 additions & 8 deletions src/components/vue-form-builder.vue
Original file line number Diff line number Diff line change
@@ -368,16 +368,18 @@
:ok-title="$t('DONE')"
ok-only
ok-variant="secondary"
header-class = "modal-header-custom"
header-class="modal-header-custom"
>
<template #modal-title>
<h5 class="modal-title">{{ $t('Edit Pages') }}</h5>
<span class="modal-subtitle">{{ $t('Change pages order and name') }}</span>
</template>
<template #modal-header-close="{ close }">
<button type="button" aria-label="Close" class="close" @click="close()">×</button>
</template>
<template #modal-title>
<h5 class="modal-title">{{ $t('Edit Pages') }}</h5>
<span class="modal-subtitle">{{ $t('Change pages order and name') }}</span>
</template>
<template #modal-header-close="{ close }">
<button type="button" aria-label="Close" class="close">×</button>
</template>

<Sortable
:fields="fields"
:items="config"
filter-key="name"
@item-edit="() => {}"
@@ -604,6 +606,12 @@ export default {
editPageIndex: null,
editPageName: "",
originalPageName: null,
fields: [
{
label: this.$t("Name"),
key: "name",
},
],
config,
confirmMessage: "",
pageDelete: 0,
30 changes: 30 additions & 0 deletions src/stories/Sortable.stories.js
Original file line number Diff line number Diff line change
@@ -59,6 +59,12 @@ export default {
// Preview the component
export const Preview = {
args: {
fields: [
{
label: "Name",
key: "name",
},
],
filterKey: "name",
items: [
{ name: "Page 1", order: 1 },
@@ -73,6 +79,12 @@ export const Preview = {
// User can reorder items
export const UserCanReorderItems = {
args: {
fields: [
{
label: "Name",
key: "name",
},
],
filterKey: "name",
items: [
{ name: "Page 1", order: 1 },
@@ -122,6 +134,12 @@ export const UserCanReorderItems = {
// User can filter by text
export const UserCanFilterByText = {
args: {
fields: [
{
label: "Name",
key: "name",
},
],
filterKey: "name",
items: [
{ name: "Zeus", order: 1 },
@@ -170,6 +188,12 @@ export const UserCanFilterByText = {
// User can sort with filter by text
export const UserCanSortWithFilterByText = {
args: {
fields: [
{
label: "Name",
key: "name",
},
],
filterKey: "name",
items: [
{ name: "Zeus", order: 1 },
@@ -238,6 +262,12 @@ export const UserCanSortWithFilterByText = {
// User can reorder items that does not have an order
export const UserCanReorderItemsThatDoesNotHaveAnOrder = {
args: {
fields: [
{
label: "Name",
key: "name",
},
],
filterKey: "name",
items: [
{ name: "Page 1" },
1 change: 1 addition & 0 deletions tests/e2e/fixtures/FOUR-13453.json

Large diffs are not rendered by default.

134 changes: 134 additions & 0 deletions tests/e2e/specs/CalcDragAndDrop.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
describe('Calcs list Drag&Drop', () => {
const clickTopBarCalcs = () => {
cy.get('[data-cy="topbar-calcs"]').click();
};

const dragAndDrop = (source, target) => {
const dataTransfer = new DataTransfer();

cy.get(source).trigger('dragstart', { dataTransfer });
cy.get(target)
.trigger('dragenter')
.trigger('dragover', { dataTransfer })
.trigger('drop', { dataTransfer });
cy.get(source).trigger('dragend');
};

beforeEach(() => {
cy.visit('/');

cy.loadFromJson('FOUR-13453.json', 0);
});

it('should drag and drop first row to third row', () => {
clickTopBarCalcs();

cy.get('[data-cy="calcs-table"] [data-test="item-1"]').eq(0).as('firstRow');
cy.get('[data-cy="calcs-table"] [data-test="item-3"]').eq(0).as('thirdRow');

cy.get('@firstRow').contains('first_name_calc');
cy.get('@thirdRow').contains('email');

dragAndDrop('@firstRow', '@thirdRow');

cy.get('[data-cy="calcs-table"] [data-test="item-1"]').eq(0).contains('last_name');
cy.get('[data-cy="calcs-table"] [data-test="item-3"]').eq(0).contains('first_name_calc');
});

it('should drag and drop second row to last row', () => {
clickTopBarCalcs();

cy.get('[data-cy="calcs-table"] [data-test="item-2"]').eq(0).as('secondRow');
cy.get('[data-cy="calcs-table"] [data-test="item-4"]').eq(0).as('lastRow');

cy.get('@secondRow').contains('last_name');
cy.get('@lastRow').contains('full_name');

dragAndDrop('@secondRow', '@lastRow');

cy.get('[data-cy="calcs-table"] [data-test="item-2"]').eq(0).contains('email');
cy.get('[data-cy="calcs-table"] [data-test="item-4"]').eq(0).contains('last_name');
});

it('should drag and drop last row to first row', () => {
clickTopBarCalcs();

cy.get('[data-cy="calcs-table"] [data-test="item-4"]').eq(0).as('lastRow');
cy.get('[data-cy="calcs-table"] [data-test="item-1"]').eq(0).as('firstRow');

cy.get('@lastRow').contains('full_name');
cy.get('@firstRow').contains('first_name');

dragAndDrop('@lastRow', '@firstRow');

cy.get('[data-cy="calcs-table"] [data-test="item-4"]').eq(0).contains('email');
cy.get('[data-cy="calcs-table"] [data-test="item-1"]').eq(0).contains('full_name');
});

it('should drag and drop to sort in ascending mode', () => {
clickTopBarCalcs();

cy.get('[data-cy="calcs-table"] [data-test="item-1"]').eq(0).as('firstRow');
cy.get('[data-cy="calcs-table"] [data-test="item-2"]').eq(0).as('secondRow');
cy.get('[data-cy="calcs-table"] [data-test="item-3"]').eq(0).as('thirdRow');
cy.get('[data-cy="calcs-table"] [data-test="item-4"]').eq(0).as('lastRow');

cy.get('@firstRow').contains('first_name');
cy.get('@secondRow').contains('last_name');
cy.get('@thirdRow').contains('email');
cy.get('@lastRow').contains('full_name');

dragAndDrop('@lastRow', '@secondRow');

cy.get('[data-cy="calcs-table"] [data-test="item-4"]').eq(0).contains('email');
cy.get('[data-cy="calcs-table"] [data-test="item-2"]').eq(0).contains('full_name');

cy.get('[data-cy="calcs-table"] [data-test="item-4"]').eq(0).as('lastRow');

dragAndDrop('@lastRow', '@firstRow');

cy.get('[data-cy="calcs-table"] [data-test="item-4"]').eq(0).contains('last_name');
cy.get('[data-cy="calcs-table"] [data-test="item-1"]').eq(0).contains('email');
});

it('should edit the name of the first calc', () => {
clickTopBarCalcs();

cy.get('[data-cy="calcs-table"] [data-test="item-1"]').eq(0).as('firstRow');

cy.get('@firstRow').contains('first_name_calc');

cy.get('@firstRow').find('[data-cy="calcs-table-edit"]').click();

cy.get('[data-cy="calcs-property-name"]').clear().type("form_input_1");
cy.get('[data-cy="calcs-button-save"]').click();

cy.get('@firstRow').contains('form_input_1');
});

it('should delete the third calc', () => {
clickTopBarCalcs();

cy.get('[data-cy="calcs-table"] [data-test="item-3"]').eq(0).as('thirdRow');

cy.get('@thirdRow').contains('email');

cy.get('@thirdRow').find('[data-cy="calcs-table-remove"]').click();

cy.get('[data-cy="calcs-table"] [data-test="item-3"]').should('not.exist');
});

it('should bypass the second calc', () => {
clickTopBarCalcs();

cy.get('[data-cy="calcs-table"] [data-test="item-2"]').eq(0).as('secondRow');

cy.get('@secondRow').contains('last_name');

cy.get('@secondRow').should('not.have.class', 'sortable-item-disabled');

cy.get('@secondRow').find('[data-test="calcs-bypass"]').click();

cy.get('@secondRow').should('have.class', 'sortable-item-disabled');
});
});
12 changes: 3 additions & 9 deletions tests/e2e/specs/ComputedFields.spec.js
Original file line number Diff line number Diff line change
@@ -45,10 +45,7 @@ describe("Computed fields", () => {
.clear()
.type("pow(form_input_2, 2)");
cy.get('[data-cy="calcs-button-save"]').click();
cy.get('[data-cy="calcs-table"]').should(
"contain.text",
"form_input_1 = form_input_2 ^ 2"
);
cy.get('[data-cy="calcs-table"]').should('contain.text', 'form_input_1');
cy.get('[data-cy="calcs-modal"] .close').click();

// Edit the created calculated property
@@ -63,10 +60,7 @@ describe("Computed fields", () => {
.clear()
.type("form_input_1 * 100");
cy.get('[data-cy="calcs-button-save"]').click();
cy.get('[data-cy="calcs-table"]').should(
"contain.text",
"form_input_2 = form_input_1 * 100"
);
cy.get('[data-cy="calcs-table"]').should('contain.text', 'form_input_2');
cy.get('[data-cy="calcs-modal"] .close').click();

// Delete the created calculated property
@@ -94,7 +88,7 @@ describe("Computed fields", () => {
cy.get('[data-cy="calcs-button-save"]').click();
cy.get('[data-cy="calcs-table"]').should(
"contain.text",
"form_input_1 = form_input_2 ^ 2"
"form_input_1"
);
cy.get('[data-cy="calcs-modal"] .close').click();