@osfarm/itineraire-technique 1.2.0 → 1.2.1

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.
@@ -0,0 +1,238 @@
1
+ /**
2
+ * DefaultLoader - Parent class for WikiLoader and ItineraLoader
3
+ * Provides common functionality for import/export and button setup
4
+ */
5
+ class DefaultLoader {
6
+ constructor(tikaeditorInstance = null) {
7
+ this.tikaeditorInstance = tikaeditorInstance;
8
+ }
9
+
10
+ /**
11
+ * Setup buttons dynamically in the toolbar
12
+ * Default implementation for standalone mode (NonWikiButtons)
13
+ * Can be overridden by child classes for Wiki/Itinera specific buttons
14
+ */
15
+ setupButtons() {
16
+ const self = this;
17
+ const container = '#toolbar-buttons-container';
18
+
19
+ // Create the NonWikiButtons div structure for standalone mode
20
+ const nonWikiButtonsDiv = $(`
21
+ <div id="NonWikiButtons" class="">
22
+ <div class="btn-group me-2" role="group">
23
+ <button type="button" class="btn btn-outline-primary primary-button btn-import-json">
24
+ <i class="fa fa-upload" aria-hidden="true"></i> Charger une rotation
25
+ </button>
26
+ <button type="button" class="btn btn-outline-primary primary-button dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-expanded="false">
27
+ <span class="visually-hidden">Autres options de chargement</span>
28
+ </button>
29
+ <ul class="dropdown-menu">
30
+ <li><a class="dropdown-item btn-import-test" href="#"><i class="fa fa-lightbulb-o" aria-hidden="true"></i> Charger un exemple</a></li>
31
+ </ul>
32
+ </div>
33
+ <button type="button" class="btn btn-outline-primary primary-button me-2 btn-export-json">
34
+ <i class="fa fa-download" aria-hidden="true"></i> Exporter (JSON)
35
+ </button>
36
+ </div>
37
+ `);
38
+
39
+ // Clear container and append the new buttons
40
+ $(container).empty().append(nonWikiButtonsDiv);
41
+
42
+ // Attach event handlers
43
+ nonWikiButtonsDiv.find('.btn-import-json').on('click', function(e) {
44
+ e.preventDefault();
45
+ self.importFromJsonFile();
46
+ });
47
+
48
+ nonWikiButtonsDiv.find('.btn-import-test').on('click', function(e) {
49
+ e.preventDefault();
50
+ self.importFromTestJson();
51
+ });
52
+
53
+ nonWikiButtonsDiv.find('.btn-export-json').on('click', function(e) {
54
+ e.preventDefault();
55
+ self.doExportToJsonFile();
56
+ });
57
+
58
+ // Add wipe button
59
+ this.addWipeButton();
60
+ }
61
+
62
+ /**
63
+ * Add the wipe button dynamically to the toolbar
64
+ */
65
+ addWipeButton() {
66
+ const self = this;
67
+ const container = '#toolbar-buttons-container';
68
+
69
+ const wipeButton = $(`
70
+ <button type="button" id="wipe-button" class="btn btn-outline-primary primary-button">
71
+ <i class="fa fa-trash" aria-hidden="true"></i> Tout effacer
72
+ </button>
73
+ `);
74
+
75
+ $(container).append(wipeButton);
76
+
77
+ wipeButton.on('click', function(e) {
78
+ e.preventDefault();
79
+ self.wipe();
80
+ });
81
+ }
82
+
83
+ /**
84
+ * Export crops to JSON file
85
+ */
86
+ doExportToJsonFile() {
87
+ let jsonName = this.tikaeditorInstance.system.title.replace(/\s+/g, '-').toLowerCase() + ".json";
88
+ this.exportToJsonFile(this.tikaeditorInstance.system, jsonName);
89
+ }
90
+
91
+ /**
92
+ * Export data to JSON file
93
+ */
94
+ exportToJsonFile(data, fileName = 'export-itk.json') {
95
+ const jsonData = JSON.stringify(data, null, 2);
96
+ const blob = new Blob([jsonData], { type: 'application/json' });
97
+ const url = URL.createObjectURL(blob);
98
+
99
+ const a = document.createElement('a');
100
+ a.href = url;
101
+ a.download = fileName;
102
+ a.click();
103
+
104
+ URL.revokeObjectURL(url);
105
+ }
106
+
107
+ /**
108
+ * Import from JSON file with confirmation
109
+ */
110
+ importFromJsonFile() {
111
+ this.showConfirmationModal(() => {
112
+ this.openFileInput();
113
+ });
114
+ }
115
+
116
+ /**
117
+ * Import from test JSON with confirmation
118
+ */
119
+ importFromTestJson() {
120
+ this.showConfirmationModal(() => {
121
+ this.importTestJSON();
122
+ });
123
+ }
124
+
125
+ /**
126
+ * Load and import test JSON file
127
+ */
128
+ importTestJSON() {
129
+ const self = this;
130
+
131
+ fetch('test/test.json')
132
+ .then(response => {
133
+ if (!response.ok) {
134
+ throw new Error("Erreur HTTP " + response.status);
135
+ }
136
+ return response.json();
137
+ })
138
+ .then(data => {
139
+ console.log("Données JSON :", data);
140
+ data.steps.forEach(step => {
141
+ let sm = new StepModel(step);
142
+ sm.setAsEdited();
143
+ });
144
+ self.tikaeditorInstance.reloadCropsFromJson(data);
145
+ })
146
+ .catch(error => {
147
+ console.error("Impossible de charger le JSON :", error);
148
+ });
149
+ }
150
+
151
+ /**
152
+ * Wipe/reset all crops data
153
+ */
154
+ wipe() {
155
+ const self = this;
156
+
157
+ const DEFAULT_TITLE = "Nouvel itinéraire technique";
158
+ this.showConfirmationModal(() => {
159
+ let crops = {
160
+ "title": DEFAULT_TITLE,
161
+ "options": {
162
+ "view": "horizontal",
163
+ "show_transcript": true,
164
+ "title_top_interventions": "Contrôle adventices",
165
+ "title_bottom_interventions": "Autres interventions",
166
+ "title_steps": "Étapes de la rotation dans la parcelle",
167
+ },
168
+ "steps": []
169
+ };
170
+
171
+ self.tikaeditorInstance.reloadCropsFromJson(crops);
172
+ });
173
+ }
174
+
175
+ /**
176
+ * Open file input dialog for JSON import
177
+ */
178
+ openFileInput() {
179
+ const self = this;
180
+ const input = document.createElement('input');
181
+ input.type = 'file';
182
+ input.accept = '.json';
183
+
184
+ input.addEventListener('change', (event) => {
185
+ const file = event.target.files[0];
186
+ if (file) {
187
+ const reader = new FileReader();
188
+ reader.onload = () => {
189
+ try {
190
+ const jsonData = JSON.parse(reader.result);
191
+ jsonData.steps.forEach(step => {
192
+ let sm = new StepModel(step);
193
+ sm.setAsEdited();
194
+ });
195
+ self.tikaeditorInstance.reloadCropsFromJson(jsonData);
196
+ } catch (error) {
197
+ console.error("Error parsing JSON file:", error);
198
+ this.showJsonErrorModal(error.message);
199
+ }
200
+ };
201
+ reader.readAsText(file);
202
+ }
203
+ });
204
+
205
+ input.click();
206
+ }
207
+
208
+ /**
209
+ * Show JSON error modal
210
+ */
211
+ showJsonErrorModal(errorMessage) {
212
+ const errorModal = new bootstrap.Modal(document.getElementById('jsonErrorModal'));
213
+ document.getElementById('jsonErrorMessage').textContent = errorMessage;
214
+ errorModal.show();
215
+ }
216
+
217
+ /**
218
+ * Show confirmation modal before potentially destructive operations
219
+ */
220
+ showConfirmationModal(onConfirm) {
221
+ if (this.tikaeditorInstance.system?.steps.length === 0)
222
+ return onConfirm();
223
+
224
+ const confirmationModal = new bootstrap.Modal(document.getElementById('confirmationModal'));
225
+ const confirmButton = document.getElementById('confirmImport');
226
+
227
+ // Avoid multiple event listeners
228
+ const newConfirmButton = confirmButton.cloneNode(true);
229
+ confirmButton.replaceWith(newConfirmButton);
230
+
231
+ newConfirmButton.addEventListener('click', () => {
232
+ confirmationModal.hide();
233
+ onConfirm();
234
+ });
235
+
236
+ confirmationModal.show();
237
+ }
238
+ }
@@ -0,0 +1,135 @@
1
+ // Encapsulate the wiki editor functionality
2
+ class ItineraLoader extends DefaultLoader {
3
+ constructor(tikaeditorInstance = null) {
4
+ super(tikaeditorInstance);
5
+
6
+ // Initialize any properties if needed
7
+ this.systemID = null;
8
+ }
9
+
10
+ /**
11
+ * Setup the Itinera buttons dynamically in the toolbar
12
+ */
13
+ setupButtons() {
14
+ const self = this;
15
+ const container = '#toolbar-buttons-container';
16
+
17
+ // Create the ItineraButtons div structure
18
+ const itineraButtonsDiv = $(`
19
+ <div id="ItineraButtons" class="">
20
+ <button type="button" class="btn btn-outline-primary primary-button btn-save-itinera">
21
+ <i class="fa fa-download" aria-hidden="true"></i> Enregistrer dans Itinera
22
+ </button>
23
+ </div>
24
+ `);
25
+
26
+ // Clear container and append the new buttons
27
+ $(container).empty().append(itineraButtonsDiv);
28
+
29
+ // Attach event handler
30
+ itineraButtonsDiv.find('.btn-save-itinera').on('click', function(e) {
31
+ e.preventDefault();
32
+ self.saveToItinera();
33
+ });
34
+
35
+ // Don't add wipe button in Itinera mode
36
+ }
37
+
38
+ /**
39
+ * When the page loads, get the URL paremeter with the target page title we want to edit:
40
+ * @returns
41
+ */
42
+ loadPageFromURL() {
43
+
44
+ const self = this;
45
+
46
+ const urlParams = new URLSearchParams(window.location.search);
47
+ self.systemID = urlParams.get('itinera');
48
+
49
+ if (!self.systemID)
50
+ return; // No page title provided, we are not in wiki edit mode
51
+
52
+ // If a page title is provided, load its content from /api/systems/1
53
+ fetch(`/api/systems/${encodeURIComponent(self.systemID)}`, {
54
+ credentials: 'same-origin'
55
+ })
56
+ .then(response => {
57
+ let res = response.json();
58
+ return res;
59
+ })
60
+ .then(data => {
61
+ if (data.json) {
62
+ try {
63
+ const content = data.json;
64
+ content.steps.forEach(step => {
65
+ let sm = new StepModel(step)
66
+ sm.setAsEdited();
67
+ });
68
+
69
+ self.tikaeditorInstance.reloadCropsFromJson(content);
70
+
71
+ let codeSnippet = `{{Graphique Triple Performance \n| title=${content.title} \n| json=${self.systemID} \n| type=Rotation }}`;
72
+ $('#code-snippet').val(codeSnippet).on('focus', function() {
73
+ $(this).select();
74
+ });
75
+ $('#codeSnippetDiv').show();
76
+
77
+
78
+ } catch (e) {
79
+ console.error("Erreur lors de l'analyse du JSON de la page :", e);
80
+ $('#jsonErrorMessage').text("Le contenu de la page n'est pas un JSON valide.");
81
+ const jsonErrorModal = new bootstrap.Modal(document.getElementById('jsonErrorModal'));
82
+ jsonErrorModal.show();
83
+ }
84
+ } else {
85
+ wipe();
86
+ }
87
+ });
88
+
89
+ }
90
+
91
+ /**
92
+ * Save a page back to the wiki. The page title must have been set previously.
93
+ */
94
+ async saveToItinera() {
95
+ const self = this;
96
+
97
+ if (!self.systemID) {
98
+ // Error ?
99
+ return;
100
+ }
101
+
102
+ // Proceed to save
103
+ const response = await fetch(`/api/systems/${encodeURIComponent(self.systemID)}`, {
104
+ method: 'PATCH',
105
+ headers: {
106
+ 'Content-Type': 'application/json'
107
+ },
108
+ credentials: 'same-origin',
109
+ body: JSON.stringify({
110
+ json: self.tikaeditorInstance.system
111
+ })
112
+ });
113
+
114
+ if (response.ok) {
115
+ // Successfully saved - show a toast
116
+ const toast = $('#liveToast');
117
+
118
+ // Update the time and message
119
+ const now = new Date();
120
+ const hours = now.getHours().toString().padStart(2, '0');
121
+ const minutes = now.getMinutes().toString().padStart(2, '0');
122
+ toast.find('small').text(`${hours}:${minutes}`);
123
+ toast.find('.toast-body').text('Sauvegardé dans Itinéra !');
124
+
125
+ const toastBootstrap = bootstrap.Toast.getOrCreateInstance(toast);
126
+ toastBootstrap.show();
127
+
128
+ } else {
129
+ // Error saving
130
+ const errorModal = new bootstrap.Modal(document.getElementById('saveErrorModal'));
131
+ errorModal.show();
132
+ }
133
+ }
134
+
135
+ }
@@ -1,10 +1,98 @@
1
1
  // Encapsulate the wiki editor functionality
2
- class WikiEditor {
3
- constructor() {
2
+ class WikiLoader extends DefaultLoader {
3
+ constructor(tikaeditorInstance = null) {
4
+ super(tikaeditorInstance);
5
+
4
6
  // Initialize any properties if needed
5
7
  this.pageTitle = null;
6
8
  }
7
9
 
10
+ /**
11
+ * Setup the Wiki buttons dynamically in the toolbar
12
+ */
13
+ setupButtons() {
14
+ const self = this;
15
+ const container = '#toolbar-buttons-container';
16
+
17
+ // Create the WikiButtons div structure
18
+ const wikiButtonsDiv = $(`
19
+ <div id="WikiButtons" class="">
20
+ <div class="btn-group me-2" role="group">
21
+ <button type="button" class="btn btn-outline-primary primary-button btn-load-wiki">
22
+ <i class="fa fa-upload" aria-hidden="true"></i> Charger une rotation
23
+ </button>
24
+ <button type="button" class="btn btn-outline-primary primary-button dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-expanded="false">
25
+ <span class="visually-hidden">Autres options de chargement</span>
26
+ </button>
27
+ <ul class="dropdown-menu">
28
+ <li><a class="dropdown-item btn-import-test" href="#"><i class="fa fa-lightbulb-o" aria-hidden="true"></i> Charger un exemple</a></li>
29
+ <li><a class="dropdown-item btn-import-json" href="#"><i class="fa fa-upload" aria-hidden="true"></i> Importer (JSON)</a></li>
30
+ </ul>
31
+ </div>
32
+ <div class="btn-group me-2" role="group">
33
+ <button type="button" class="btn btn-outline-primary primary-button btn-save-wiki">
34
+ <i class="fa fa-download" aria-hidden="true"></i> Enregistrer dans le wiki
35
+ </button>
36
+ <button type="button" class="btn btn-outline-primary primary-button dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-expanded="false">
37
+ <span class="visually-hidden">Autres options d'enregistrement</span>
38
+ </button>
39
+ <ul class="dropdown-menu">
40
+ <li><a class="dropdown-item btn-save-as" href="#"><i class="fa fa-save" aria-hidden="true"></i> Enregistrer sous</a></li>
41
+ <li><a class="dropdown-item btn-export-json" href="#"><i class="fa fa-download" aria-hidden="true"></i> Exporter</a></li>
42
+ </ul>
43
+ </div>
44
+ </div>
45
+ `);
46
+
47
+ // Clear container and append the new buttons
48
+ $(container).empty().append(wikiButtonsDiv);
49
+
50
+ // Attach event handlers
51
+ wikiButtonsDiv.find('.btn-load-wiki').on('click', function(e) {
52
+ e.preventDefault();
53
+ self.showConfirmationModal(() => {
54
+ self.loadFromWiki();
55
+ });
56
+ });
57
+
58
+ wikiButtonsDiv.find('.btn-save-wiki').on('click', function(e) {
59
+ e.preventDefault();
60
+ self.saveToWiki();
61
+ });
62
+
63
+ wikiButtonsDiv.find('.btn-save-as').on('click', function(e) {
64
+ e.preventDefault();
65
+ self.showSaveAsModal();
66
+ });
67
+
68
+ wikiButtonsDiv.find('.btn-import-test').on('click', function(e) {
69
+ e.preventDefault();
70
+ self.importFromTestJson();
71
+ });
72
+
73
+ wikiButtonsDiv.find('.btn-import-json').on('click', function(e) {
74
+ e.preventDefault();
75
+ self.importFromJsonFile();
76
+ });
77
+
78
+ wikiButtonsDiv.find('.btn-export-json').on('click', function(e) {
79
+ e.preventDefault();
80
+ self.doExportToJsonFile();
81
+ });
82
+
83
+ // Add wipe button
84
+ this.addWipeButton();
85
+
86
+ // Setup Save As modal event listeners
87
+ $('#saveAsUseExistingPage').on('change', function() {
88
+ $('#saveAsPageSelect').prop('disabled', !this.checked);
89
+ });
90
+
91
+ $('#saveAsConfirmBtn').off('click').on('click', function() {
92
+ self.saveAs();
93
+ });
94
+ }
95
+
8
96
  /**
9
97
  * When the page loads, get the URL paremeter with the target page title we want to edit:
10
98
  * @returns
@@ -35,14 +123,14 @@ class WikiEditor {
35
123
  let sm = new StepModel(step)
36
124
  sm.setAsEdited();
37
125
  });
38
- reloadCropsFromJson(content);
126
+ self.tikaeditorInstance.reloadCropsFromJson(content);
39
127
 
40
128
  let codeSnippet = `{{Graphique Triple Performance \n| title=${content.title} \n| json=${self.pageTitle} \n| type=Rotation }}`;
41
129
  $('#code-snippet').val(codeSnippet).on('focus', function() {
42
130
  $(this).select();
43
131
  });
44
- $('#codeSnippetDiv').removeClass('d-none');
45
-
132
+
133
+ $('#codeSnippetDiv').show();
46
134
 
47
135
  } catch (e) {
48
136
  console.error("Erreur lors de l'analyse du JSON de la page :", e);
@@ -78,7 +166,7 @@ class WikiEditor {
78
166
  }
79
167
 
80
168
  // If a page title is provided, save to that page
81
- self.savePageToWiki(self.pageTitle, JSON.stringify(crops, null, 2))
169
+ self.savePageToWiki(self.pageTitle, JSON.stringify(self.tikaeditorInstance.system, null, 2))
82
170
  .then(async () => {
83
171
  alert("Itinéraire technique enregistré avec succès !");
84
172
  })
@@ -278,8 +366,8 @@ class WikiEditor {
278
366
 
279
367
  // Set default filename from title if it's been changed
280
368
  const filenameInput = $('#saveAsFilename');
281
- const currentTitle = crops.title || '';
282
- if (crops.defaultTitle === false) {
369
+ const currentTitle = self.tikaeditorInstance.system.title || '';
370
+ if (self.tikaeditorInstance.system.defaultTitle === false) {
283
371
  filenameInput.val(currentTitle);
284
372
  } else {
285
373
  filenameInput.val('');
@@ -315,8 +403,8 @@ class WikiEditor {
315
403
  // Build the final URL: subpagename/filename.json
316
404
  const finalUrl = `${subpageName}/${filename}.json`;
317
405
 
318
- if (crops.defaultTitle !== false) {
319
- crops.title = filename;
406
+ if (self.tikaeditorInstance.system.defaultTitle !== false) {
407
+ self.tikaeditorInstance.system.title = filename;
320
408
  }
321
409
 
322
410
  return finalUrl;
@@ -340,7 +428,7 @@ class WikiEditor {
340
428
  self.pageTitle = url;
341
429
 
342
430
  // Save to the wiki first
343
- await self.savePageToWiki(self.pageTitle, JSON.stringify(crops, null, 2));
431
+ await self.savePageToWiki(self.pageTitle, JSON.stringify(self.tikaeditorInstance.system, null, 2));
344
432
 
345
433
  // Close the modal
346
434
  const modal = bootstrap.Modal.getInstance(document.getElementById('saveAsModal'));