@osfarm/itineraire-technique 1.2.3 → 1.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- .main-header{background-color:#6fa76f;color:#fff;height:3rem;display:flex;align-items:center}.main-header .btn.show,.main-header .btn:first-child:active,.main-header :not(.btn-check)+.btn:active{background-color:#026602}.editor-view{overflow-y:auto;height:calc(100vh - 4rem)}.rotation_item .step-edit{color:#878787;cursor:pointer;display:none;margin-left:10px;font-size:70%;vertical-align:super}.rotation_item:hover .step-edit{display:inline !important}.welcome-view{background-color:#6fa76f;color:#fff;padding:1rem;margin-top:1rem;border-radius:1rem}.welcome-view #cropsContainer{color:green}.card-white{background-color:#fff;padding:1rem;border-radius:1rem}.card-holder{background-color:#f5f5f5;padding:1rem;border-radius:1rem;margin:auto}.editable-row{background-color:#f7f7f7;min-height:3rem;display:flex;align-items:center;border-radius:.5rem}.editable-row .edit-buttons{visibility:hidden}.editable-row:hover>.edit-buttons{visibility:visible}.intervention-row{background:#e0e0e0}.primary-button{color:#fff;background-color:green;border:green}.primary-button:hover{background-color:#026602}.close-step-times{background:none;border:none;color:#878787;font-size:120%}.close-step-times:hover{color:#494949}#cropsContainer .drag-handle{color:#ccc;font-weight:normal;font-size:86%;margin-right:10px;vertical-align:middle;cursor:grab}#cropsContainer div.col{cursor:pointer}.form-control.text-right{text-align:right}.modal .form-label{font-weight:600}#code-snippet{font-family:monospace;font-size:13px;text-align:left;border:1px inset;background-color:#f1f1f1}/*# sourceMappingURL=styles-editor.css.map */
1
+ .main-header{background-color:#6fa76f;color:#fff;height:3rem;display:flex;align-items:center}.main-header .btn.show,.main-header .btn:first-child:active,.main-header :not(.btn-check)+.btn:active{background-color:#026602}.editor-view{overflow-y:auto;height:calc(100vh - 4rem)}.rotation_item .step-edit{color:#878787;cursor:pointer;display:none;margin-left:10px;font-size:70%;vertical-align:super}.rotation_item:hover .step-edit{display:inline !important}.welcome-view{background-color:#6fa76f;color:#fff;padding:1rem;margin-top:1rem;border-radius:1rem}.welcome-view #cropsContainer{color:green}.card-white{background-color:#fff;padding:1rem;border-radius:1rem}.card-holder{background-color:#f5f5f5;padding:1rem;border-radius:1rem;margin:auto}@keyframes step-highlight{0%{box-shadow:0 0 0 3px #ffc107}80%{box-shadow:0 0 0 3px #ffc107}100%{box-shadow:0 0 0 0px rgba(0,0,0,0)}}.editable-row{background-color:#f7f7f7;min-height:3rem;display:flex;align-items:center;border-radius:.5rem}.editable-row .edit-buttons{visibility:hidden}.editable-row:hover>.edit-buttons{visibility:visible}.editable-row.step-shifted{animation:step-highlight 1.4s ease-out forwards}.editable-row.step-shifted .edit-buttons{visibility:visible}.intervention-row{background:#e0e0e0}.primary-button{color:#fff;background-color:green;border:green}.primary-button:hover{background-color:#026602}.close-step-times{background:none;border:none;color:#878787;font-size:120%}.close-step-times:hover{color:#494949}#cropsContainer .drag-handle{color:#ccc;font-weight:normal;font-size:86%;margin-right:10px;vertical-align:middle;cursor:grab}#cropsContainer div.col{cursor:pointer}.form-control.text-right{text-align:right}.modal .form-label{font-weight:600}#code-snippet{font-family:monospace;font-size:13px;text-align:left;border:1px inset;background-color:#f1f1f1}/*# sourceMappingURL=styles-editor.css.map */
@@ -1 +1 @@
1
- {"version":3,"sourceRoot":"","sources":["../scss/styles-editor.scss"],"names":[],"mappings":"AAKA,aACI,yBACA,WACA,OALiB,KAOjB,aACA,mBAEA,sGAGI,yBAIR,aACI,gBAGA,0BAKA,0BACI,cACA,eACA,aACA,iBACA,cACA,qBAGJ,gCACI,0BAIR,cACI,yBACA,WACA,aACA,gBACA,mBAEA,8BACI,YAIR,YACI,sBACA,aACA,mBAGJ,aACI,yBACA,aACA,mBACA,YAGJ,cACI,yBACA,gBAEA,aACA,mBAEA,oBAEA,4BACI,kBAGJ,kCACI,mBAIR,kBACI,mBAGJ,gBACI,MAzFiB,KA0FjB,iBA5FiB,MA6FjB,OA7FiB,MA+FjB,sBACI,iBA/Fa,QAmGrB,kBACI,gBACA,YACA,cACA,eAEA,wBACI,cAKJ,6BACI,WACA,mBACA,cACA,kBACA,sBACA,YAGJ,wBACI,eAKJ,yBACI,iBAKJ,mBACI,gBAIR,cACI,sBACA,eACA,gBACA,iBACA","file":"styles-editor.css"}
1
+ {"version":3,"sourceRoot":"","sources":["../scss/styles-editor.scss"],"names":[],"mappings":"AAKA,aACI,yBACA,WACA,OALiB,KAOjB,aACA,mBAEA,sGAGI,yBAIR,aACI,gBAGA,0BAKA,0BACI,cACA,eACA,aACA,iBACA,cACA,qBAGJ,gCACI,0BAIR,cACI,yBACA,WACA,aACA,gBACA,mBAEA,8BACI,YAIR,YACI,sBACA,aACA,mBAGJ,aACI,yBACA,aACA,mBACA,YAGJ,0BACI,gCACA,iCACA,yCAGJ,cACI,yBACA,gBAEA,aACA,mBAEA,oBAEA,4BACI,kBAGJ,kCACI,mBAGJ,2BACI,gDAEA,yCACI,mBAKZ,kBACI,mBAGJ,gBACI,MAvGiB,KAwGjB,iBA1GiB,MA2GjB,OA3GiB,MA6GjB,sBACI,iBA7Ga,QAiHrB,kBACI,gBACA,YACA,cACA,eAEA,wBACI,cAKJ,6BACI,WACA,mBACA,cACA,kBACA,sBACA,YAGJ,wBACI,eAKJ,yBACI,iBAKJ,mBACI,gBAIR,cACI,sBACA,eACA,gBACA,iBACA","file":"styles-editor.css"}
@@ -5,6 +5,31 @@
5
5
  class DefaultLoader {
6
6
  constructor(tikaeditorInstance = null) {
7
7
  this.tikaeditorInstance = tikaeditorInstance;
8
+ this.hasUnsavedChanges = false;
9
+
10
+ // Warn the user before leaving the page if there are unsaved changes
11
+ const self = this;
12
+ window.addEventListener('beforeunload', function(e) {
13
+ if (self.hasUnsavedChanges) {
14
+ e.preventDefault();
15
+ }
16
+ });
17
+
18
+ // Mark dirty on any data mutation
19
+ if (tikaeditorInstance) {
20
+ const originalRefresh = tikaeditorInstance.refreshAllTables.bind(tikaeditorInstance);
21
+ tikaeditorInstance.refreshAllTables = function() {
22
+ self.hasUnsavedChanges = true;
23
+ return originalRefresh();
24
+ };
25
+ }
26
+ }
27
+
28
+ /**
29
+ * Mark the editor as having no unsaved changes
30
+ */
31
+ markClean() {
32
+ this.hasUnsavedChanges = false;
8
33
  }
9
34
 
10
35
  /**
@@ -15,7 +40,7 @@ class DefaultLoader {
15
40
  setupButtons() {
16
41
  const self = this;
17
42
  const container = '#toolbar-buttons-container';
18
-
43
+
19
44
  // Create the NonWikiButtons div structure for standalone mode
20
45
  const nonWikiButtonsDiv = $(`
21
46
  <div id="NonWikiButtons" class="">
@@ -35,26 +60,26 @@ class DefaultLoader {
35
60
  </button>
36
61
  </div>
37
62
  `);
38
-
63
+
39
64
  // Clear container and append the new buttons
40
65
  $(container).empty().append(nonWikiButtonsDiv);
41
-
66
+
42
67
  // Attach event handlers
43
68
  nonWikiButtonsDiv.find('.btn-import-json').on('click', function(e) {
44
69
  e.preventDefault();
45
70
  self.importFromJsonFile();
46
71
  });
47
-
72
+
48
73
  nonWikiButtonsDiv.find('.btn-import-test').on('click', function(e) {
49
74
  e.preventDefault();
50
75
  self.importFromTestJson();
51
76
  });
52
-
77
+
53
78
  nonWikiButtonsDiv.find('.btn-export-json').on('click', function(e) {
54
79
  e.preventDefault();
55
80
  self.doExportToJsonFile();
56
81
  });
57
-
82
+
58
83
  // Add wipe button
59
84
  this.addWipeButton();
60
85
  }
@@ -65,15 +90,15 @@ class DefaultLoader {
65
90
  addWipeButton() {
66
91
  const self = this;
67
92
  const container = '#toolbar-buttons-container';
68
-
93
+
69
94
  const wipeButton = $(`
70
95
  <button type="button" id="wipe-button" class="btn btn-outline-primary primary-button">
71
96
  <i class="fa fa-trash" aria-hidden="true"></i> Tout effacer
72
97
  </button>
73
98
  `);
74
-
99
+
75
100
  $(container).append(wipeButton);
76
-
101
+
77
102
  wipeButton.on('click', function(e) {
78
103
  e.preventDefault();
79
104
  self.wipe();
@@ -86,6 +111,7 @@ class DefaultLoader {
86
111
  doExportToJsonFile() {
87
112
  let jsonName = this.tikaeditorInstance.system.title.replace(/\s+/g, '-').toLowerCase() + ".json";
88
113
  this.exportToJsonFile(this.tikaeditorInstance.system, jsonName);
114
+ this.markClean();
89
115
  }
90
116
 
91
117
  /**
@@ -169,6 +195,7 @@ class DefaultLoader {
169
195
  };
170
196
 
171
197
  self.tikaeditorInstance.reloadCropsFromJson(crops);
198
+ self.markClean();
172
199
  });
173
200
  }
174
201
 
@@ -13,7 +13,7 @@ class ItineraLoader extends DefaultLoader {
13
13
  setupButtons() {
14
14
  const self = this;
15
15
  const container = '#toolbar-buttons-container';
16
-
16
+
17
17
  // Create the ItineraButtons div structure
18
18
  const itineraButtonsDiv = $(`
19
19
  <div id="ItineraButtons" class="">
@@ -22,25 +22,25 @@ class ItineraLoader extends DefaultLoader {
22
22
  </button>
23
23
  </div>
24
24
  `);
25
-
25
+
26
26
  // Clear container and append the new buttons
27
27
  $(container).empty().append(itineraButtonsDiv);
28
-
28
+
29
29
  // Attach event handler
30
30
  itineraButtonsDiv.find('.btn-save-itinera').on('click', function(e) {
31
31
  e.preventDefault();
32
32
  self.saveToItinera();
33
33
  });
34
-
34
+
35
35
  // Don't add wipe button in Itinera mode
36
36
  }
37
37
 
38
38
  /**
39
39
  * When the page loads, get the URL paremeter with the target page title we want to edit:
40
- * @returns
40
+ * @returns
41
41
  */
42
42
  loadPageFromURL() {
43
-
43
+
44
44
  const self = this;
45
45
 
46
46
  const urlParams = new URLSearchParams(window.location.search);
@@ -51,7 +51,7 @@ class ItineraLoader extends DefaultLoader {
51
51
  return; // No page title provided, we are not in wiki edit mode
52
52
 
53
53
  // Build API URL with UUID if provided
54
- const apiUrl = uuid
54
+ const apiUrl = uuid
55
55
  ? `/api/systems/${encodeURIComponent(self.systemID)}?uuid=${encodeURIComponent(uuid)}`
56
56
  : `/api/systems/${encodeURIComponent(self.systemID)}`;
57
57
 
@@ -69,7 +69,7 @@ class ItineraLoader extends DefaultLoader {
69
69
  // Extract UUID from redirectTo path (format: /project/22/UUID)
70
70
  const pathParts = data.redirectTo.split('/');
71
71
  const redirectUuid = pathParts[pathParts.length - 1];
72
-
72
+
73
73
  if (redirectUuid) {
74
74
  // Retry with UUID parameter
75
75
  return fetch(`/api/systems/${encodeURIComponent(self.systemID)}?uuid=${redirectUuid}`, {
@@ -87,15 +87,16 @@ class ItineraLoader extends DefaultLoader {
87
87
  let sm = new StepModel(step)
88
88
  sm.setAsEdited();
89
89
  });
90
-
90
+
91
91
  self.tikaeditorInstance.reloadCropsFromJson(content);
92
+ self.markClean();
92
93
 
93
94
  let codeSnippet = `{{Graphique Triple Performance \n| title=${content.title} \n| json=${self.systemID} \n| type=Rotation }}`;
94
95
  $('#code-snippet').val(codeSnippet).on('focus', function() {
95
96
  $(this).select();
96
97
  });
97
98
  $('#codeSnippetDiv').show();
98
-
99
+
99
100
 
100
101
  } catch (e) {
101
102
  console.error("Erreur lors de l'analyse du JSON de la page :", e);
@@ -107,7 +108,7 @@ class ItineraLoader extends DefaultLoader {
107
108
  self.wipe();
108
109
  }
109
110
  });
110
-
111
+
111
112
  }
112
113
 
113
114
  /**
@@ -120,7 +121,7 @@ class ItineraLoader extends DefaultLoader {
120
121
  // Error ?
121
122
  return;
122
123
  }
123
-
124
+
124
125
  // Proceed to save
125
126
  const response = await fetch(`/api/systems/${encodeURIComponent(self.systemID)}`, {
126
127
  method: 'PATCH',
@@ -142,12 +143,13 @@ class ItineraLoader extends DefaultLoader {
142
143
 
143
144
  if (response.ok) {
144
145
  // Successfully saved - show a toast
146
+ self.markClean();
145
147
 
146
148
  toast.find('.toast-body').text('Sauvegardé dans Itinéra !');
147
149
 
148
150
  const toastBootstrap = bootstrap.Toast.getOrCreateInstance(toast);
149
151
  toastBootstrap.show();
150
-
152
+
151
153
  } else {
152
154
  // Error saving
153
155
 
@@ -13,7 +13,7 @@ class WikiLoader extends DefaultLoader {
13
13
  setupButtons() {
14
14
  const self = this;
15
15
  const container = '#toolbar-buttons-container';
16
-
16
+
17
17
  // Create the WikiButtons div structure
18
18
  const wikiButtonsDiv = $(`
19
19
  <div id="WikiButtons" class="">
@@ -43,10 +43,10 @@ class WikiLoader extends DefaultLoader {
43
43
  </div>
44
44
  </div>
45
45
  `);
46
-
46
+
47
47
  // Clear container and append the new buttons
48
48
  $(container).empty().append(wikiButtonsDiv);
49
-
49
+
50
50
  // Attach event handlers
51
51
  wikiButtonsDiv.find('.btn-load-wiki').on('click', function(e) {
52
52
  e.preventDefault();
@@ -54,32 +54,32 @@ class WikiLoader extends DefaultLoader {
54
54
  self.loadFromWiki();
55
55
  });
56
56
  });
57
-
57
+
58
58
  wikiButtonsDiv.find('.btn-save-wiki').on('click', function(e) {
59
59
  e.preventDefault();
60
60
  self.saveToWiki();
61
61
  });
62
-
62
+
63
63
  wikiButtonsDiv.find('.btn-save-as').on('click', function(e) {
64
64
  e.preventDefault();
65
65
  self.showSaveAsModal();
66
66
  });
67
-
67
+
68
68
  wikiButtonsDiv.find('.btn-import-test').on('click', function(e) {
69
69
  e.preventDefault();
70
70
  self.importFromTestJson();
71
71
  });
72
-
72
+
73
73
  wikiButtonsDiv.find('.btn-import-json').on('click', function(e) {
74
74
  e.preventDefault();
75
75
  self.importFromJsonFile();
76
76
  });
77
-
77
+
78
78
  wikiButtonsDiv.find('.btn-export-json').on('click', function(e) {
79
79
  e.preventDefault();
80
80
  self.doExportToJsonFile();
81
81
  });
82
-
82
+
83
83
  // Add wipe button
84
84
  this.addWipeButton();
85
85
 
@@ -95,10 +95,10 @@ class WikiLoader extends DefaultLoader {
95
95
 
96
96
  /**
97
97
  * When the page loads, get the URL paremeter with the target page title we want to edit:
98
- * @returns
98
+ * @returns
99
99
  */
100
100
  loadPageFromURL() {
101
-
101
+
102
102
  const self = this;
103
103
 
104
104
  const urlParams = new URLSearchParams(window.location.search);
@@ -124,6 +124,7 @@ class WikiLoader extends DefaultLoader {
124
124
  sm.setAsEdited();
125
125
  });
126
126
  self.tikaeditorInstance.reloadCropsFromJson(content);
127
+ self.markClean();
127
128
 
128
129
  let codeSnippet = `{{Graphique Triple Performance \n| title=${content.title} \n| json=${self.pageTitle} \n| type=Rotation }}`;
129
130
  $('#code-snippet').val(codeSnippet).on('focus', function() {
@@ -151,7 +152,7 @@ class WikiLoader extends DefaultLoader {
151
152
  }
152
153
  }
153
154
  });
154
-
155
+
155
156
  }
156
157
 
157
158
  /**
@@ -164,10 +165,11 @@ class WikiLoader extends DefaultLoader {
164
165
  self.showSaveAsModal();
165
166
  return;
166
167
  }
167
-
168
+
168
169
  // If a page title is provided, save to that page
169
170
  self.savePageToWiki(self.pageTitle, JSON.stringify(self.tikaeditorInstance.system, null, 2))
170
171
  .then(async () => {
172
+ self.markClean();
171
173
  alert("Itinéraire technique enregistré avec succès !");
172
174
  })
173
175
  .catch(err => {
@@ -204,7 +206,7 @@ class WikiLoader extends DefaultLoader {
204
206
  const editData = await editResp.json();
205
207
  console.log(editData);
206
208
 
207
- if (editData.edit && editData.edit.result === 'Success') {
209
+ if (editData.edit && editData.edit.result === 'Success') {
208
210
  return Promise.resolve();
209
211
  } else {
210
212
  return Promise.reject(editData);
@@ -230,7 +232,7 @@ class WikiLoader extends DefaultLoader {
230
232
  const encodedUsername = 'User:' + username;
231
233
  const query = encodeURIComponent(`[[Page author::${encodedUsername}]][[~*.json]]`);
232
234
  const url = `/api.php?action=ask&query=${query}|sort=Modification date|order=desc&format=json`;
233
-
235
+
234
236
  const response = await fetch(url, {
235
237
  credentials: 'include'
236
238
  });
@@ -249,7 +251,7 @@ class WikiLoader extends DefaultLoader {
249
251
  const encodedUsername = 'User:' + username;
250
252
  const query = encodeURIComponent(`[[Page author::${encodedUsername}]]`);
251
253
  const url = `/api.php?action=ask&query=${query}|sort=Modification date|order=desc&format=json`;
252
-
254
+
253
255
  const response = await fetch(url, {
254
256
  credentials: 'include'
255
257
  });
@@ -265,40 +267,40 @@ class WikiLoader extends DefaultLoader {
265
267
  // Show the modal
266
268
  const modal = new bootstrap.Modal(document.getElementById('wikiFilesModal'));
267
269
  modal.show();
268
-
270
+
269
271
  // Reset the modal content
270
272
  $('#wikiFilesStatus').html('<i class="fa fa-spinner fa-spin"></i> Chargement de vos fichiers...');
271
273
  $('#wikiFilesList').empty();
272
-
274
+
273
275
  try {
274
276
  // Get user info
275
277
  const userInfo = await this.getWikiUserInfo();
276
-
278
+
277
279
  if (!userInfo.id || userInfo.id === 0) {
278
280
  $('#wikiFilesStatus').html('<div class="alert alert-warning">Vous devez être connecté au wiki pour utiliser cette fonctionnalité.</div>');
279
281
  return;
280
282
  }
281
-
283
+
282
284
  // Get user's JSON files
283
285
  const filesData = await this.getWikiUserFiles(userInfo.name);
284
-
286
+
285
287
  // Parse the results
286
288
  const results = filesData.query?.results;
287
-
289
+
288
290
  if (!results || Object.keys(results).length === 0) {
289
291
  $('#wikiFilesStatus').html('<div class="alert alert-info">Aucun fichier JSON trouvé dans vos pages.</div>');
290
292
  return;
291
293
  }
292
-
294
+
293
295
  // Display the files
294
296
  $('#wikiFilesStatus').html(`<p class="text-muted">Connecté en tant que <strong>${userInfo.name}</strong></p>`);
295
-
297
+
296
298
  const filesList = $('#wikiFilesList');
297
-
299
+
298
300
  for (const [pageTitle, pageData] of Object.entries(results)) {
299
301
  const displayTitle = pageData.displaytitle || pageTitle;
300
302
  const fullUrl = pageData.fullurl || '';
301
-
303
+
302
304
  // if displayTitle is a subpage (contains /), split in two spans:
303
305
  let [parent, child] = displayTitle.includes('/') ? displayTitle.split('/') : [displayTitle];
304
306
 
@@ -321,10 +323,10 @@ class WikiLoader extends DefaultLoader {
321
323
  </div>
322
324
  </a>
323
325
  `);
324
-
326
+
325
327
  filesList.append(listItem);
326
328
  }
327
-
329
+
328
330
  } catch (error) {
329
331
  $('#wikiFilesStatus').html(`<div class="alert alert-danger">Erreur lors du chargement des fichiers: ${error.message}</div>`);
330
332
  }
@@ -335,11 +337,11 @@ class WikiLoader extends DefaultLoader {
335
337
  */
336
338
  async showSaveAsModal() {
337
339
  const self = this;
338
-
340
+
339
341
  try {
340
342
  // Get user info to check if logged in
341
343
  const userInfo = await this.getWikiUserInfo();
342
-
344
+
343
345
  if (!userInfo.id || userInfo.id === 0) {
344
346
  alert('Vous devez être connecté au wiki pour utiliser cette fonctionnalité.');
345
347
  return;
@@ -347,23 +349,23 @@ class WikiLoader extends DefaultLoader {
347
349
 
348
350
  // Get user's existing pages
349
351
  const pagesData = await this.getWikiUserPages(userInfo.name);
350
-
352
+
351
353
  // Show the modal
352
354
  const modal = new bootstrap.Modal(document.getElementById('saveAsModal'));
353
355
  modal.show();
354
-
356
+
355
357
  // Populate the select with user's pages
356
358
  const pageSelect = $('#saveAsPageSelect');
357
359
  pageSelect.empty();
358
360
  pageSelect.append('<option value="">Sélectionner une page...</option>');
359
-
361
+
360
362
  if (pagesData.query?.results) {
361
363
  for (const [pageTitle, pageData] of Object.entries(pagesData.query.results)) {
362
364
  const displayTitle = pageData.displaytitle || pageTitle;
363
365
  pageSelect.append(`<option value="${pageTitle}">${displayTitle}</option>`);
364
366
  }
365
367
  }
366
-
368
+
367
369
  // Set default filename from title if it's been changed
368
370
  const filenameInput = $('#saveAsFilename');
369
371
  const currentTitle = self.tikaeditorInstance.system.title || '';
@@ -372,7 +374,7 @@ class WikiLoader extends DefaultLoader {
372
374
  } else {
373
375
  filenameInput.val('');
374
376
  }
375
-
377
+
376
378
  } catch (error) {
377
379
  console.error("Error showing save as modal:", error);
378
380
  alert('Erreur lors du chargement des données utilisateur.');
@@ -387,20 +389,20 @@ class WikiLoader extends DefaultLoader {
387
389
  const useExistingPage = $('#saveAsUseExistingPage').prop('checked');
388
390
  const selectedPage = $('#saveAsPageSelect').val();
389
391
  const filename = $('#saveAsFilename').val().trim();
390
-
392
+
391
393
  if (!filename) {
392
394
  alert('Veuillez saisir un nom de fichier.');
393
395
  return null;
394
396
  }
395
-
397
+
396
398
  let subpageName;
397
-
399
+
398
400
  if (useExistingPage && selectedPage) {
399
401
  subpageName = selectedPage;
400
402
  } else {
401
403
  subpageName = 'Itinéraires techniques non classés';
402
404
  }
403
-
405
+
404
406
  // Build the final URL: subpagename/filename.json
405
407
  const finalUrl = `${subpageName}/${filename}.json`;
406
408
 
@@ -416,11 +418,11 @@ class WikiLoader extends DefaultLoader {
416
418
  */
417
419
  async saveAs() {
418
420
  const url = this.buildSaveAsUrl();
419
-
421
+
420
422
  if (!url) {
421
423
  return; // Error already handled in buildSaveAsUrl
422
424
  }
423
-
425
+
424
426
  const self = this;
425
427
  const oldPageTitle = self.pageTitle;
426
428
 
@@ -430,17 +432,18 @@ class WikiLoader extends DefaultLoader {
430
432
 
431
433
  // Save to the wiki first
432
434
  await self.savePageToWiki(self.pageTitle, JSON.stringify(self.tikaeditorInstance.system, null, 2));
433
-
435
+ self.markClean();
436
+
434
437
  // Close the modal
435
438
  const modal = bootstrap.Modal.getInstance(document.getElementById('saveAsModal'));
436
439
  modal.hide();
437
-
440
+
438
441
  alert(`Itinéraire technique enregistré avec succès sous "${url}"`);
439
-
442
+
440
443
  // Only navigate after successful save
441
444
  const newEditorUrl = `editor.html?wiki=${encodeURIComponent(self.pageTitle)}`;
442
445
  window.location.href = newEditorUrl;
443
-
446
+
444
447
  } catch (error) {
445
448
  // Restore the old page title if save failed
446
449
  self.pageTitle = oldPageTitle;
package/js/editor-main.js CHANGED
@@ -5,7 +5,7 @@
5
5
  class TikaEditor {
6
6
  constructor() {
7
7
  this.DEFAULT_TITLE = "Nouvel itinéraire technique";
8
-
8
+
9
9
  // Initialize the crops data structure
10
10
  this.system = {
11
11
  "title": this.DEFAULT_TITLE,
@@ -48,7 +48,6 @@ class TikaEditor {
48
48
  this.enableTitleEditing();
49
49
  this.setupCloseStepButtons();
50
50
  this.setupCropFormKeydown();
51
- this.setupCropsSortable();
52
51
  this.setupParamsModal();
53
52
  }
54
53
 
@@ -101,7 +100,7 @@ class TikaEditor {
101
100
  let movedStep = new StepModel(step);
102
101
  let duration = movedStep.getDurationInDays();
103
102
 
104
- if (lastStepEnd != null) {
103
+ if (lastStepEnd != null) {
105
104
  lastStepEnd.setDate(lastStepEnd.getDate() + 1);
106
105
 
107
106
  movedStep.setStartDate(lastStepEnd);
@@ -135,7 +134,7 @@ class TikaEditor {
135
134
  */
136
135
  setupParamsModal() {
137
136
  const self = this;
138
-
137
+
139
138
  $('#modalParams').on('show.bs.modal', function (event) {
140
139
  // Set the modal form inputs values from crops.options
141
140
  $("#viewSelect").val(self.system.options.view);
@@ -147,16 +146,16 @@ class TikaEditor {
147
146
  $("#addressInput").val(self.system.options.address ?? "");
148
147
  $("#latitudeInput").val(self.system.options.latitude ?? "");
149
148
  $("#longitudeInput").val(self.system.options.longitude ?? "");
150
-
149
+
151
150
  // Load ombrothermic data from climate_data if present
152
- let hasClimateData = self.system.options.climate_data &&
153
- self.system.options.climate_data.temperatures &&
151
+ let hasClimateData = self.system.options.climate_data &&
152
+ self.system.options.climate_data.temperatures &&
154
153
  self.system.options.climate_data.precipitations;
155
-
154
+
156
155
  // Check the checkbox if show_climate_diagram is explicitly true OR if climate_data exists
157
156
  let showDiagram = self.system.options.show_climate_diagram === true;
158
157
  $("#ombroCheck").prop("checked", showDiagram);
159
-
158
+
160
159
  if (hasClimateData) {
161
160
  let tempLine = self.system.options.climate_data.temperatures.join(' ');
162
161
  let precipLine = self.system.options.climate_data.precipitations.join(' ');
@@ -164,21 +163,21 @@ class TikaEditor {
164
163
  } else {
165
164
  $("#ombroData").val("");
166
165
  }
167
-
166
+
168
167
  // Enable/disable textarea based on checkbox state
169
168
  $("#ombroData").prop("disabled", !showDiagram);
170
169
  });
171
-
170
+
172
171
  // Add event listener to toggle textarea when checkbox changes
173
172
  $("#ombroCheck").on("change", function() {
174
173
  $("#ombroData").prop("disabled", !this.checked);
175
174
  });
176
-
175
+
177
176
  // Update Google Maps link when coordinates change
178
177
  function updateGoogleMapsLink() {
179
178
  const lat = $("#latitudeInput").val().trim();
180
179
  const lon = $("#longitudeInput").val().trim();
181
-
180
+
182
181
  if (lat && lon && !isNaN(parseFloat(lat)) && !isNaN(parseFloat(lon))) {
183
182
  const mapsUrl = `https://www.google.com/maps?q=${lat},${lon}`;
184
183
  $("#googleMapsLink a").attr("href", mapsUrl);
@@ -187,28 +186,28 @@ class TikaEditor {
187
186
  $("#googleMapsLink").hide();
188
187
  }
189
188
  }
190
-
189
+
191
190
  // Attach listeners to lat/long inputs
192
191
  $("#latitudeInput, #longitudeInput").on("input change", updateGoogleMapsLink);
193
-
192
+
194
193
  // Update link when modal opens
195
194
  $('#modalParams').on('shown.bs.modal', function() {
196
195
  updateGoogleMapsLink();
197
196
  });
198
-
197
+
199
198
  // Location search button handler
200
199
  $("#searchLocationBtn").on("click", function() {
201
200
  const address = $("#addressInput").val().trim();
202
-
201
+
203
202
  if (!address) {
204
203
  $("#locationSearchStatus").html('<span class="text-warning">Veuillez entrer une adresse</span>');
205
204
  return;
206
205
  }
207
-
206
+
208
207
  // Show loading state
209
208
  $("#searchLocationBtn").prop("disabled", true);
210
209
  $("#locationSearchStatus").html('<i class="fa fa-spinner fa-spin"></i> Recherche en cours...');
211
-
210
+
212
211
  $.ajax({
213
212
  url: "https://itk-info.tripleperformance.fr/api/location",
214
213
  method: "POST",
@@ -216,14 +215,14 @@ class TikaEditor {
216
215
  data: JSON.stringify({ address: address }),
217
216
  success: function(data) {
218
217
  console.log("Location data received:", data);
219
-
218
+
220
219
  // Populate latitude and longitude
221
220
  if (data.latitude && data.longitude) {
222
221
  $("#latitudeInput").val(data.latitude);
223
222
  $("#longitudeInput").val(data.longitude);
224
223
  updateGoogleMapsLink();
225
224
  }
226
-
225
+
227
226
  // Populate climate data if available
228
227
  if (data.monthly_temperatures && data.monthly_rainfall) {
229
228
  let tempLine = data.monthly_temperatures.join(' ');
@@ -232,7 +231,7 @@ class TikaEditor {
232
231
  $("#ombroCheck").prop("checked", true);
233
232
  $("#ombroData").prop("disabled", false);
234
233
  }
235
-
234
+
236
235
  // Show success message
237
236
  let message = '<span class="text-success">✓ Coordonnées trouvées';
238
237
  if (data.source_explanation) {
@@ -261,20 +260,20 @@ class TikaEditor {
261
260
  self.system.options.address = $("#addressInput").val().trim();
262
261
  self.system.options.latitude = $("#latitudeInput").val().trim();
263
262
  self.system.options.longitude = $("#longitudeInput").val().trim();
264
-
263
+
265
264
  // Convert ombrothermic data to climate_data object
266
265
  let ombroEnabled = $("#ombroCheck").prop("checked");
267
-
266
+
268
267
  if (ombroEnabled) {
269
268
  self.system.options.show_climate_diagram = true;
270
-
269
+
271
270
  let ombroText = $("#ombroData").val().trim();
272
271
  let lines = ombroText.split('\n');
273
-
272
+
274
273
  if (lines.length >= 2) {
275
274
  let temperatures = lines[0].trim().split(/\s+/).map(v => parseFloat(v)).filter(v => !isNaN(v));
276
275
  let precipitations = lines[1].trim().split(/\s+/).map(v => parseFloat(v)).filter(v => !isNaN(v));
277
-
276
+
278
277
  if (temperatures.length > 0 && precipitations.length > 0) {
279
278
  self.system.options.climate_data = {
280
279
  temperatures: temperatures,
@@ -312,18 +311,18 @@ class TikaEditor {
312
311
  const self = this;
313
312
  $(document).ready(function() {
314
313
  // If we are in a wiki (the domain contains "tripleperformance.ag or tripleperformance.fr" then show the Wiki buttons
315
- if (window.location.hostname.includes("tripleperformance.ag") || window.location.hostname.includes("tripleperformance.fr")) {
314
+ if ((window.location.hostname.includes("itinera") || window.location.hostname.includes("localhost"))
315
+ && window.location.search.includes("itinera") ) {
316
316
 
317
- // Hide NonWikiButtons
318
- self.editorLoader = new WikiLoader(self);
317
+ // If we are in Itinera - the domain is *.itinera.ag or localhost with a itinera param
318
+ self.editorLoader = new ItineraLoader(self);
319
319
  self.editorLoader.setupButtons();
320
320
  self.editorLoader.loadPageFromURL();
321
321
 
322
- } else if (window.location.hostname.includes("itinera.ag") ||
323
- (window.location.hostname.includes("localhost") && window.location.search.includes("itinera")) ) {
322
+ } else if (window.location.hostname.includes("tripleperformance.ag") || window.location.hostname.includes("tripleperformance.fr")) {
324
323
 
325
- // If we are in Itinera - the domain is *.itinera.ag or localhost with a itinera param
326
- self.editorLoader = new ItineraLoader(self);
324
+ // Hide NonWikiButtons
325
+ self.editorLoader = new WikiLoader(self);
327
326
  self.editorLoader.setupButtons();
328
327
  self.editorLoader.loadPageFromURL();
329
328
 
@@ -367,6 +366,7 @@ class TikaEditor {
367
366
  */
368
367
  updateSelectedStep() {
369
368
  this.selectedStep.updateFromForm();
369
+ this.sortStepsByStartDate();
370
370
  this.refreshAllTables();
371
371
  }
372
372
 
@@ -377,10 +377,35 @@ class TikaEditor {
377
377
  document.getElementById(elementId).value = value;
378
378
  }
379
379
 
380
+ /**
381
+ * Briefly highlight a step row to signal it has been moved
382
+ */
383
+ highlightStepRow(id) {
384
+ let row = $('#cropsContainer .step-row[data-id="' + id + '"]');
385
+ if (!row.length) return;
386
+ row.removeClass('step-shifted');
387
+ // Force reflow so the animation restarts on repeated clicks
388
+ void row[0].offsetWidth;
389
+ row.addClass('step-shifted');
390
+ row.one('animationend', function () {
391
+ $(this).removeClass('step-shifted');
392
+ });
393
+ }
394
+
395
+ /**
396
+ * Sort steps array by start date (ascending)
397
+ */
398
+ sortStepsByStartDate() {
399
+ this.system.steps.sort(function (a, b) {
400
+ return new Date(a.startDate) - new Date(b.startDate);
401
+ });
402
+ }
403
+
380
404
  /**
381
405
  * Refresh all tables and views
382
406
  */
383
407
  refreshAllTables() {
408
+ this.sortStepsByStartDate();
384
409
  this.refreshStepsButtonList();
385
410
  this.renderChart();
386
411
  }
@@ -400,7 +425,7 @@ class TikaEditor {
400
425
  hideStepEditor() {
401
426
  $('#cropListView').show();
402
427
  $('#welcomeView').hide();
403
- $('#cropEditorView').hide();
428
+ $('#cropEditorView').hide();
404
429
  }
405
430
 
406
431
  /**
@@ -414,7 +439,7 @@ class TikaEditor {
414
439
  $('.step-edit').click(function (event) {
415
440
  event.stopPropagation();
416
441
  let stepId = renderer.getElementID(event.target.closest('.rotation_item'));
417
-
442
+
418
443
  let index = stepId.split('_')[1];
419
444
  self.selectedStep = new StepModel(self.system.steps[index]);
420
445
  self.selectStep(self.selectedStep);
@@ -487,12 +512,12 @@ class TikaEditor {
487
512
  }),
488
513
  success: function(data) {
489
514
  console.log("Réponse:", data);
490
-
515
+
491
516
  if (data.color_hex && self.selectedStep.step.useDefaultColor) {
492
517
  self.setInputValue("cropColor", data.color_hex);
493
518
  self.updateSelectedStep();
494
519
  }
495
-
520
+
496
521
  let startDate = self.getRotationEndDate();
497
522
  if (data.average_sowing_date && self.selectedStep.step.useDefaultStartDate) {
498
523
  // data.average_sowing_date is in MM-DD format, we need to convert it to YYYY-MM-DD
@@ -520,7 +545,7 @@ class TikaEditor {
520
545
  self.setInputValue("cropEndDate", endDate.toISOString().split('T')[0]);
521
546
  self.updateSelectedStep();
522
547
  }
523
-
548
+
524
549
  if (data.source_explanation) {
525
550
  $('#itk-api-comment').text(data.source_explanation);
526
551
  } else {
@@ -530,7 +555,7 @@ class TikaEditor {
530
555
  error: function(err) {
531
556
  console.error("Erreur:", err);
532
557
  }
533
- });
558
+ });
534
559
  }
535
560
 
536
561
  /**
@@ -572,7 +597,7 @@ class TikaEditor {
572
597
 
573
598
  this.InterventionTableManager.setupDiv();
574
599
  this.InterventionTableManager.refreshInterventionsTable(this.selectedStep);
575
-
600
+
576
601
  this.refreshAllTables();
577
602
  }
578
603
 
@@ -614,17 +639,29 @@ class TikaEditor {
614
639
  const rowDiv = this.createCropRow(crop);
615
640
  cropsContainer.append(rowDiv);
616
641
  });
617
-
618
- cropsContainer.sortable("refresh");
619
642
  }
620
643
 
621
- addEditAndRemoveButtons(rowDiv, deleteId, editFunction, deleteFunction, duplicateFunction, style="btn-group") {
644
+ addEditAndRemoveButtons(rowDiv, deleteId, editFunction, deleteFunction, duplicateFunction, shiftYearUpFunction, shiftYearDownFunction, style="btn-group") {
622
645
  rowDiv = $(rowDiv);
623
646
 
624
647
  let actionContainer = $(`<div class="col-auto edit-buttons m-1 ${style}" role="group"></div>`);
625
648
 
626
649
  rowDiv.append(actionContainer);
627
650
 
651
+ if (shiftYearUpFunction != null) {
652
+ actionContainer.append($('<button class="btn btn-outline-secondary p-2" title="Reculer d\'un an"><i class="fa fa-arrow-up"></i></button>').click(function (event) {
653
+ event.stopPropagation();
654
+ shiftYearUpFunction(deleteId);
655
+ }));
656
+ }
657
+
658
+ if (shiftYearDownFunction != null) {
659
+ actionContainer.append($('<button class="btn btn-outline-secondary p-2" title="Avancer d\'un an"><i class="fa fa-arrow-down"></i></button>').click(function (event) {
660
+ event.stopPropagation();
661
+ shiftYearDownFunction(deleteId);
662
+ }));
663
+ }
664
+
628
665
  actionContainer.append($('<button class="edit-button btn btn-outline-primary p-2"><i class="fa fa-pencil"></i></button>').click(function(event) {
629
666
  event.stopPropagation();
630
667
  editFunction();
@@ -657,12 +694,17 @@ class TikaEditor {
657
694
 
658
695
  let rowDiv = $('<div class="row mb-2 step-row editable-row position-relative" data-id="'+step.getStep().id +'"></div>');
659
696
 
697
+ let startDate = new Date(step.getStep().startDate);
698
+ let startDateStr = startDate.toLocaleDateString('fr-FR', { month: 'short', year: 'numeric' });
699
+ startDateStr = startDateStr.charAt(0).toUpperCase() + startDateStr.slice(1);
700
+
660
701
  rowDiv.append($('<div class="col"></div>')
661
- .append($('<i class="fa fa-bars drag-handle" aria-hidden="true"></i>'))
662
- .append($('<strong>' + step.getStep().name + '</strong>')));
702
+ .append($('<strong>' + step.getStep().name + '</strong>'))
703
+ .append($('<br>'))
704
+ .append($('<small class="text-muted">' + startDateStr + '</small>')));
663
705
 
664
- this.addEditAndRemoveButtons(rowDiv,
665
- step.getStep().id,
706
+ this.addEditAndRemoveButtons(rowDiv,
707
+ step.getStep().id,
666
708
  function () {
667
709
  console.log("Selected step:", step.getStep().name);
668
710
  self.selectStep(step);
@@ -674,6 +716,30 @@ class TikaEditor {
674
716
  },
675
717
  function(id) {
676
718
  self.duplicateStep(id);
719
+ },
720
+ function(id) {
721
+ let s = self.system.steps.find(function (c) { return c.id == id; });
722
+ if (!s) return;
723
+ let start = new Date(s.startDate);
724
+ let end = new Date(s.endDate);
725
+ start.setFullYear(start.getFullYear() - 1);
726
+ end.setFullYear(end.getFullYear() - 1);
727
+ s.startDate = start;
728
+ s.endDate = end;
729
+ self.refreshAllTables();
730
+ self.highlightStepRow(id);
731
+ },
732
+ function(id) {
733
+ let s = self.system.steps.find(function (c) { return c.id == id; });
734
+ if (!s) return;
735
+ let start = new Date(s.startDate);
736
+ let end = new Date(s.endDate);
737
+ start.setFullYear(start.getFullYear() + 1);
738
+ end.setFullYear(end.getFullYear() + 1);
739
+ s.startDate = start;
740
+ s.endDate = end;
741
+ self.refreshAllTables();
742
+ self.highlightStepRow(id);
677
743
  });
678
744
 
679
745
  rowDiv.click();
@@ -730,7 +796,7 @@ class TikaEditor {
730
796
 
731
797
  // Create a StepModel instance to ensure proper initialization
732
798
  let stepModel = new StepModel(newStep);
733
-
799
+
734
800
  // Add the duplicated step to the rotation
735
801
  this.system.steps.push(stepModel.getStep());
736
802
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@osfarm/itineraire-technique",
3
- "version": "1.2.3",
3
+ "version": "1.2.5",
4
4
  "description": "A visualisation tool to show agricultural technical itineraries based on Echarts",
5
5
  "main": "editor.html",
6
6
  "scripts": {
@@ -66,6 +66,12 @@ $header-height : 3rem;
66
66
  margin : auto
67
67
  }
68
68
 
69
+ @keyframes step-highlight {
70
+ 0% { box-shadow: 0 0 0 3px #ffc107; }
71
+ 80% { box-shadow: 0 0 0 3px #ffc107; }
72
+ 100% { box-shadow: 0 0 0 0px transparent; }
73
+ }
74
+
69
75
  .editable-row {
70
76
  background-color: #f7f7f7;
71
77
  min-height : 3rem;
@@ -82,6 +88,14 @@ $header-height : 3rem;
82
88
  &:hover>.edit-buttons {
83
89
  visibility: visible;
84
90
  }
91
+
92
+ &.step-shifted {
93
+ animation: step-highlight 1.4s ease-out forwards;
94
+
95
+ .edit-buttons {
96
+ visibility: visible;
97
+ }
98
+ }
85
99
  }
86
100
 
87
101
  .intervention-row {