Commit 6f3fdc32 authored by Julien BOUYER's avatar Julien BOUYER
Browse files

Merge branch 'feature/UNOTOPLYS-171_reordonner_ecrans' into 'develop'

UNOTOPLYS-171 feat(ordre écran) : réordonner les écrans;

See merge request !64
parents 951f85fb f1e0bd1f
This diff is collapsed.
......@@ -135,6 +135,11 @@
<artifactId>hibernate-types-52</artifactId>
<version>${hibernate-types.version}</version>
</dependency>
<dependency>
<groupId>com.github.java-json-tools</groupId>
<artifactId>json-patch</artifactId>
<version>1.13</version>
</dependency>
</dependencies>
</dependencyManagement>
......@@ -389,6 +394,10 @@
<groupId>com.vladmihalcea</groupId>
<artifactId>hibernate-types-52</artifactId>
</dependency>
<dependency>
<groupId>com.github.java-json-tools</groupId>
<artifactId>json-patch</artifactId>
</dependency>
</dependencies>
<build>
......
......@@ -106,10 +106,34 @@ public interface ScreenRepository extends JpaRepository<Screen, Long> {
* @param formId un identifiant de formulaire
* @param index un index (position) d'écran
*/
@Query(
value = "UPDATE screen SET index = (index - 1) WHERE index > :index AND form_id = :formId",
nativeQuery = true
)
@Query(value = "UPDATE screen SET index = (index - 1) WHERE index > :index AND form_id = :formId", nativeQuery = true)
@Modifying(flushAutomatically = true, clearAutomatically = true)
void decreaseScreenIndex(@Param("formId") Long formId, @Param("index") Integer index);
/**
* Déplace vers le bas tous les écrans dont l'index est >= nouvel index et < ancien index
* @param oldIndex l'ancienne position de l'écran
* @param newIndex la nouvelle position de l'écran
*/
@Query(value = "UPDATE screen SET index = index + 1 WHERE index >= :newIndex AND index < :oldIndex", nativeQuery = true)
@Modifying
void moveUp(@Param("oldIndex") Integer oldIndex, @Param("newIndex") Integer newIndex);
/**
* Déplace vers le haut tous les écrans dont l'index est > ancien index et <= nouvel index
* @param oldIndex l'ancienne position de l'écran
* @param newIndex la nouvelle position de l'écran
*/
@Query(value = "UPDATE screen SET index = index - 1 WHERE index > :oldIndex AND index <= :newIndex", nativeQuery = true)
@Modifying
void moveDown(@Param("oldIndex") Integer oldIndex, @Param("newIndex") Integer newIndex);
/**
* Change l'index
* @param id l'id d'un écran
* @param newIndex son nouvel index
*/
@Query(value = "UPDATE screen SET index = :newIndex WHERE id = :id", nativeQuery = true)
@Modifying(flushAutomatically = true, clearAutomatically = true)
void updateIndex(@Param("id") Long id, @Param("newIndex") Integer newIndex);
}
......@@ -12,6 +12,11 @@ import org.springframework.security.access.AccessDeniedException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.fge.jsonpatch.JsonPatch;
import com.github.fge.jsonpatch.JsonPatchException;
import com.unantes.orientactive.domain.Screen;
import com.unantes.orientactive.domain.User;
import com.unantes.orientactive.repository.ScreenRepository;
......@@ -213,4 +218,55 @@ public class ScreenService {
public Optional<ScreenDTO> findScreenByIndexAndFormId(final Integer index, final Long formId) {
return screenRepository.findScreenByIndexAndFormId(index, formId).map(screenMapper::toDto);
}
/**
* Modifie les élements de l'écran ciblés par le patch. Si l'index est modifié : met à jour les écrans impactés.
* @param id un identifiant d'écran en base de donnée
* @param patch le patch à appliquer
* @return
*/
public ScreenDTO patch(final Long id, final JsonPatch patch) {
final ScreenDTO oldScreen = findOne(id).orElseThrow(() -> new EntityNotFoundException(id));
final ObjectMapper objectMapper = new ObjectMapper();
final JsonNode screenNode = objectMapper.convertValue(oldScreen, JsonNode.class);
try {
final JsonNode nodePatched = patch.apply(screenNode);
final ScreenDTO patchedScreen = objectMapper.treeToValue(nodePatched, ScreenDTO.class);
updateScreenOrder(oldScreen, patchedScreen);
return patchedScreen;
} catch (JsonPatchException | JsonProcessingException e) {
throw new IllegalStateException(e);
}
}
/**
* Met à jour si nécessaire l'ordre d'un écran et ceux des écrans impactés
* @param oldScreen l'ancien écran
* @param patchedScreen l'écran modifié
*/
public void updateScreenOrder(final ScreenDTO oldScreen, final ScreenDTO patchedScreen) {
final Integer newIndex = patchedScreen.getIndex();
final Integer oldIndex = oldScreen.getIndex();
if (!newIndex.equals(oldIndex)) {
updateScreenOrder(oldScreen.getId(), oldIndex, newIndex);
}
}
/**
* Met à jour l'ordre d'un écran et ceux des écrans impactés
* @param id l'ancien écran
* @param oldIndex l'écran modifié
* @param newIndex l'écran modifié
*/
private void updateScreenOrder(final Long id, final Integer oldIndex, final Integer newIndex) {
if (oldIndex < newIndex) {
screenRepository.moveDown(oldIndex, newIndex);
screenRepository.updateIndex(id, newIndex);
} else if (oldIndex > newIndex) {
screenRepository.moveUp(oldIndex, newIndex);
screenRepository.updateIndex(id, newIndex);
} else {
log.debug("L'index de l'écran id = {} n'a pas été modifié", id);
}
}
}
......@@ -7,6 +7,7 @@ import javax.validation.constraints.NotNull;
import org.apache.commons.lang3.builder.ToStringBuilder;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.unantes.orientactive.converter.bean.Item;
import com.unantes.orientactive.validation.CheckJson;
......@@ -52,6 +53,7 @@ public class ScreenDTO implements Serializable {
private List<NextScreenExpressionDTO> nextScreenExpressions;
@JsonIgnore
public Integer getNextIndex() {
return index + 1;
}
......
package com.unantes.orientactive.service.dto;
/**
* Représente l'index d'un écran
*/
public class ScreenIndexDTO {
private Long id;
private int index;
public int getIndex() {
return index;
}
public void setIndex(final int index) {
this.index = index;
}
public Long getId() {
return id;
}
public void setId(final Long id) {
this.id = id;
}
}
package com.unantes.orientactive.web.rest;
import com.github.fge.jsonpatch.JsonPatch;
import com.unantes.orientactive.domain.User;
import com.unantes.orientactive.navigation.exception.ScreenNotFoundException;
import com.unantes.orientactive.service.ScreenService;
......@@ -14,6 +15,7 @@ import org.springframework.http.ResponseEntity;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
......@@ -108,6 +110,11 @@ public class ScreenResource {
.body(result);
}
@PatchMapping(path = "/screens/{id}", consumes = "application/json-patch+json")
public ResponseEntity<ScreenDTO> patch(@PathVariable Long id, @RequestBody JsonPatch patch) {
return ResponseEntity.ok().body(screenService.patch(id, patch));
}
/**
* {@code GET /screens/:id} : get the "id" screen.
*
......
import Vue from 'vue';
import { Component, Prop } from 'vue-property-decorator';
import { IScreen } from '@/shared/model/screen.model';
/**
* Composant d'affichage d'une liste de IScreen avec possibilité de déplacer les éléments par drag-n-drop.
*/
@Component
export default class DndList extends Vue {
@Prop()
public screenList: any[];
@Prop()
public idScreen: number;
private currentList: any[];
constructor() {
super();
// La liste passée en prop n'est pas utilisée directement car le dnd doit pouvoir la mettre à jour.
// On la copie dans une data qui est bindée à la vue par le méthode set et get
this.currentList = this.screenList;
}
public set list(screenList: any[]) {
this.currentList = screenList;
}
public get list(): any[] {
return this.currentList;
}
public get options() {
return {
dropzoneSelector: 'ol',
draggableSelector: 'li',
excludeOlderBrowsers: true,
showDropzoneAreas: true,
};
}
public isCurrentPanel(panel: any): boolean {
return this.idScreen == panel.id;
}
/**
* Emet un évènement à la sélection d'un Screen indiquant .
* @param {IScreen} panelScreen écran sélectionné dans le panneau
* @returns {void}
*/
public selectScreen(panelScreen: IScreen): void {
this.$emit('selectScreen', panelScreen);
}
/**
* Méthode appelée après le drag-n-drop d'un élément sur sa nouvelle position.
* Calcule le nouvel index de l'élément et émet un événement indiquant le nouvel index pour l'écran.
* @param {CustomEvent} e événement émis après le déplacement d'un évènement
*/
public reordered(e: CustomEvent) {
const selectedElm = e.detail.items[0];
const screenIdx = this.screenList.findIndex(s => s.id === Number.parseInt(selectedElm.getAttribute('data-screenid'), 10));
const screen = this.screenList[screenIdx];
// L'index donné est celui dans le tableau des screens (0 à n), or à l'affichage, on démarre à 1
let newIndex = e.detail.index;
if (newIndex <= screenIdx) {
// Si le nouvel index est inférieur à l'index courant (déplacement vers le haut),
// il est donc nécessaire de l'incrémenté de 1 (position de départ).
// Lors d'un déplacement vers le bas, l'index est déjà incrémenté de 1
// (l'écran en cours de déplacement est toujours présent dans la liste)
newIndex++;
}
this.$emit('update', { screen, newIndex });
}
}
<template>
<div v-drag-and-drop:options="options">
<ol class="p-2 text-sm select-none" @reordered="reordered">
<li
v-for="panelScreen in list"
:key="panelScreen.index"
:class="[
'flex items-center p-1 leading-tight transition rounded-md cursor-pointer group hover:absolute focus:ring ',
isCurrentPanel(panelScreen) ? 'font-medium text-white bg-blue-600 hover:bg-blue-700' : 'hover:bg-blue-100',
]"
tabindex="0"
:data-screenid="panelScreen.id"
role="button"
@click="selectScreen(panelScreen)"
>
<span
:class="[
'flex-shrink-0 inline-block w-8 h-6 mr-2 text-sm text-center border rounded-md',
isCurrentPanel(panelScreen)
? 'text-blue-900 bg-blue-100 border-white group-hover:border-blue-900-400'
: 'bg-white border-gray-300 group-hover:border-gray-400',
]"
>{{ panelScreen.index }}</span
>{{ panelScreen.titleBo }}
</li>
</ol>
</div>
</template>
<script lang="ts" src="./dnd-list.component.ts" />
import Vue from 'vue';
import { Component, Inject } from 'vue-property-decorator';
import { Component, Inject, Watch } from 'vue-property-decorator';
import ScreenService from '@/entities/screen/screen.service';
import { IScreen } from '@/shared/model/screen.model';
import ScreenService from '@/entities/screen/screen.service';
import DndList from '@/components/dnd-list/dnd-list.vue';
import Confirm from '@/components/confirm/confirm.vue';
import Icon from '@/components/icon/icon.vue';
@Component({
components: {
DndList,
Confirm,
Icon,
},
......@@ -16,19 +18,37 @@ import Icon from '@/components/icon/icon.vue';
export default class Panel extends Vue {
@Inject('screenService') private screenService: () => ScreenService;
public screenList: any[];
/**
* Clé de MAJ du composant DND. On l'incrémente à chaque mise à jour de
* screenList pour indiquer au composant qu'il doit se recharger.
*/
public dndKey: number;
public showConfirm: boolean;
constructor() {
super();
// Si getter screenList est déjà prêt, on le copie, sinon on charge une liste vide
this.screenList = this.$store.getters.screenList || [];
this.dndKey = 0;
this.showConfirm = false;
}
public get screenList(): boolean {
return this.$store.getters.screenList;
/**
* Surveille la MAJ du getter screenList et met à jour la data screenList à chaque modification.
* Incrémente dndKey pour forcer le composant DndList à se réfraîchir.
*/
@Watch('$store.getters.screenList')
public reloadScreenList() {
this.screenList = this.$store.getters.screenList;
this.dndKey += 1;
}
public isCurrentPanel(panel) {
return this.idScreen == panel.id;
public updateScreenIndex({ screen, newIndex }) {
this.$store.dispatch('updateScreenIndex', { screen, newIndex });
this.dndKey += 1;
}
public get idWorkspace(): string {
......@@ -39,18 +59,18 @@ export default class Panel extends Vue {
return this.$store.getters.idForm;
}
public get idScreen(): string {
public get idScreen(): number {
return this.$store.getters.idScreen;
}
public selectScreen(screen) {
public selectScreen(screen: IScreen) {
if (screen.id != this.idScreen) {
this.$router.push({
name: 'ScreenEditComponent',
params: {
idWorkspace: this.idWorkspace,
idForm: this.idForm,
idScreen: screen.id,
idScreen: `${screen.id}`,
},
});
}
......
......@@ -23,29 +23,7 @@
@confirm="deleteScreen"
/>
</div>
<ol class="p-2 text-sm select-none">
<li
v-for="panelScreen in screenList"
:key="panelScreen.index"
:class="[
'flex items-center p-1 leading-tight transition rounded-md cursor-pointer group hover:absolute focus:ring ',
isCurrentPanel(panelScreen) ? 'font-medium text-white bg-blue-600 hover:bg-blue-700' : 'hover:bg-blue-100',
]"
tabindex="0"
role="button"
@click="selectScreen(panelScreen)"
>
<span
:class="[
'flex-shrink-0 inline-block w-8 h-6 mr-2 text-sm text-center border rounded-md',
isCurrentPanel(panelScreen)
? 'text-blue-900 bg-blue-100 border-white group-hover:border-blue-900-400'
: 'bg-white border-gray-300 group-hover:border-gray-400',
]"
>{{ panelScreen.index }}</span
>{{ panelScreen.titleBo }}
</li>
</ol>
<dnd-list :screenList="screenList" :idScreen="idScreen" @update="updateScreenIndex" @selectScreen="selectScreen" :key="dndKey" />
</div>
</aside>
</template>
......
......@@ -95,4 +95,19 @@ export default class ScreenService {
});
});
}
public updateIndex(id: number, index: number): Promise<IScreen> {
return new Promise<IScreen>((resolve, reject) => {
axios
.patch(`${baseApiUrl}/${id}`, [{ op: 'replace', path: '/index', value: index }], {
headers: { 'Content-Type': 'application/json-patch+json' },
})
.then(res => {
resolve(res.data);
})
.catch(err => {
reject(err);
});
});
}
}
......@@ -3,6 +3,7 @@
import Vue from 'vue';
import App from './app.vue';
import Vue2Filters from 'vue2-filters';
import VueDraggable from 'vue-draggable';
import router from './router';
import { metaInfo } from './shared/meta';
import * as config from './shared/config/config';
......@@ -18,6 +19,7 @@ import HeaderService from '@/shared/service/header-service';
Vue.config.productionTip = false;
config.initVueApp(Vue);
Vue.use(Vue2Filters);
Vue.use(VueDraggable);
const i18n = config.initI18N(Vue);
const store = config.initVueXStore(Vue);
......
import { Module } from 'vuex';
import ScreenService from '@/entities/screen/screen.service';
import { IScreen } from '@/shared/model/screen.model';
const screenService = new ScreenService();
......@@ -15,6 +16,14 @@ export const formStore: Module<any, any> = {
screenList: state => state.screenList,
},
actions: {
updateScreenIndex({ state, dispatch }, { screen, newIndex }) {
screenService
.updateIndex(screen.id, newIndex)
.then(() => {
dispatch('loadScreens', state.idForm);
})
.catch(console.error);
},
loadScreens({ commit }, idForm) {
screenService.findByFormId(idForm).then(res => {
commit('setScreenList', res);
......
......@@ -33,3 +33,18 @@
content: '/';
@apply inline-block px-4 font-light text-gray-200;
}
@keyframes nodeInserted {
from {
@apply opacity-20;
}
to {
@apply opacity-80;
}
}
.item-dropzone-area {
animation-duration: 0.5s;
animation-name: nodeInserted;
@apply bg-gray-400 h-8 opacity-80 rounded-md;
}
......@@ -28,6 +28,7 @@ import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.Matchers.hasItem;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
......@@ -75,6 +76,10 @@ public class ScreenResourceIT {
public static final String SCREEN_2 = "screen2";
public static final String SCREEN_3 = "screen3";
public static final String SCREEN_4 = "screen4";
private List<NextScreenExpression> nextScreenExpressions;
@Autowired
......@@ -409,4 +414,101 @@ public class ScreenResourceIT {
Assert.assertEquals(1, em.createNativeQuery("select * from next_screen_expression").getResultList().size());
}
@Test
@Transactional
public void moveDownScreenTest() throws Exception {
Screen screen2 = new Screen().name(SCREEN_2).nameBo(SCREEN_2).index(2).form(form).reference(SCREEN_2).description(SCREEN_2).defaultNextScreenReference(DEFAULT_NEXT_SCREEN_REFERENCE).items(DEFAULT_ITEMS).nextScreenExpressions(nextScreenExpressions);
Screen screen3 = new Screen().name(SCREEN_3).nameBo(SCREEN_3).index(3).form(form).reference(SCREEN_3).description(SCREEN_3).defaultNextScreenReference(DEFAULT_NEXT_SCREEN_REFERENCE).items(DEFAULT_ITEMS).nextScreenExpressions(nextScreenExpressions);
Screen screen4 = new Screen().name(SCREEN_4).nameBo(SCREEN_4).index(4).form(form).reference(SCREEN_4).description(SCREEN_4).defaultNextScreenReference(DEFAULT_NEXT_SCREEN_REFERENCE).items(DEFAULT_ITEMS).nextScreenExpressions(nextScreenExpressions);
// Initialize the database
screenRepository.save(screen);
screenRepository.save(screen2);
screenRepository.save(screen3);
screenRepository.save(screen4);
// on descend l'écran deux à la place du 3
restScreenMockMvc
.perform(patch("/api/screens/{id}", screen2.getId())
.contentType("application/json-patch+json")
.content("[{ \"op\":\"replace\", \"path\":\"/index\", \"value\":\"3\"}]"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.index").value("3"));
checkIndex(screen.getReference(), (Integer) 1);
checkIndex(screen3.getReference(), (Integer) 2);
checkIndex(screen2.getReference(), (Integer) 3);
checkIndex(screen4.getReference(), (Integer) 4);
}
@Test
@Transactional
public void moveBottomScreenTest() throws Exception {
Screen screen2 = new Screen().name(SCREEN_2).nameBo(SCREEN_2).index(2).form(form).reference(SCREEN_2).description(SCREEN_2).defaultNextScreenReference(DEFAULT_NEXT_SCREEN_REFERENCE).items(DEFAULT_ITEMS).nextScreenExpressions(nextScreenExpressions);
Screen screen3 = new Screen().name(SCREEN_3).nameBo(SCREEN_3).index(3).form(form).reference(SCREEN_3).description(SCREEN_3).defaultNextScreenReference(DEFAULT_NEXT_SCREEN_REFERENCE).items(DEFAULT_ITEMS).nextScreenExpressions(nextScreenExpressions);
Screen screen4 = new Screen().name(SCREEN_4).nameBo(SCREEN_4).index(4).form(form).reference(SCREEN_4).description(SCREEN_4).defaultNextScreenReference(DEFAULT_NEXT_SCREEN_REFERENCE).items(DEFAULT_ITEMS).nextScreenExpressions(nextScreenExpressions);
// Initialize the database
screenRepository.save(screen);
screenRepository.save(screen2);
screenRepository.save(screen3);
screenRepository.save(screen4);
// on descend l'écran deux à la place du 3
restScreenMockMvc
.perform(patch("/api/screens/{id}", screen2.getId())