@underpostnet/underpost 2.97.0 → 2.97.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.
Files changed (78) hide show
  1. package/README.md +2 -2
  2. package/baremetal/commission-workflows.json +33 -3
  3. package/bin/deploy.js +1 -1
  4. package/cli.md +7 -2
  5. package/conf.js +3 -0
  6. package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
  7. package/manifests/deployment/dd-test-development/deployment.yaml +2 -2
  8. package/package.json +1 -1
  9. package/packer/scripts/fuse-tar-root +3 -3
  10. package/scripts/disk-clean.sh +23 -23
  11. package/scripts/gpu-diag.sh +2 -2
  12. package/scripts/ip-info.sh +11 -11
  13. package/scripts/maas-upload-boot-resource.sh +1 -1
  14. package/scripts/nvim.sh +1 -1
  15. package/scripts/packer-setup.sh +13 -13
  16. package/scripts/rocky-setup.sh +2 -2
  17. package/scripts/rpmfusion-ffmpeg-setup.sh +4 -4
  18. package/scripts/ssl.sh +7 -7
  19. package/src/api/core/core.service.js +0 -5
  20. package/src/api/default/default.service.js +7 -5
  21. package/src/api/document/document.model.js +30 -1
  22. package/src/api/document/document.router.js +6 -0
  23. package/src/api/document/document.service.js +423 -51
  24. package/src/api/file/file.model.js +112 -4
  25. package/src/api/file/file.ref.json +42 -0
  26. package/src/api/file/file.service.js +380 -32
  27. package/src/api/user/user.model.js +38 -1
  28. package/src/api/user/user.router.js +96 -63
  29. package/src/api/user/user.service.js +81 -48
  30. package/src/cli/baremetal.js +689 -329
  31. package/src/cli/cluster.js +50 -52
  32. package/src/cli/db.js +424 -166
  33. package/src/cli/deploy.js +1 -1
  34. package/src/cli/index.js +12 -1
  35. package/src/cli/lxd.js +3 -3
  36. package/src/cli/repository.js +1 -1
  37. package/src/cli/run.js +2 -1
  38. package/src/cli/ssh.js +10 -10
  39. package/src/client/components/core/Account.js +327 -36
  40. package/src/client/components/core/AgGrid.js +3 -0
  41. package/src/client/components/core/Auth.js +9 -3
  42. package/src/client/components/core/Chat.js +2 -2
  43. package/src/client/components/core/Content.js +159 -78
  44. package/src/client/components/core/Css.js +16 -2
  45. package/src/client/components/core/CssCore.js +16 -12
  46. package/src/client/components/core/FileExplorer.js +115 -8
  47. package/src/client/components/core/Input.js +204 -11
  48. package/src/client/components/core/LogIn.js +42 -20
  49. package/src/client/components/core/Modal.js +257 -177
  50. package/src/client/components/core/Panel.js +324 -27
  51. package/src/client/components/core/PanelForm.js +280 -73
  52. package/src/client/components/core/PublicProfile.js +888 -0
  53. package/src/client/components/core/Router.js +117 -15
  54. package/src/client/components/core/SearchBox.js +1117 -0
  55. package/src/client/components/core/SignUp.js +26 -7
  56. package/src/client/components/core/SocketIo.js +6 -3
  57. package/src/client/components/core/Translate.js +98 -0
  58. package/src/client/components/core/Validator.js +15 -0
  59. package/src/client/components/core/windowGetDimensions.js +6 -6
  60. package/src/client/components/default/MenuDefault.js +59 -12
  61. package/src/client/components/default/RoutesDefault.js +1 -0
  62. package/src/client/services/core/core.service.js +163 -1
  63. package/src/client/services/default/default.management.js +451 -64
  64. package/src/client/services/default/default.service.js +13 -6
  65. package/src/client/services/document/document.service.js +23 -0
  66. package/src/client/services/file/file.service.js +43 -16
  67. package/src/client/services/user/user.service.js +13 -9
  68. package/src/db/DataBaseProvider.js +1 -1
  69. package/src/db/mongo/MongooseDB.js +1 -1
  70. package/src/index.js +1 -1
  71. package/src/mailer/MailerProvider.js +4 -4
  72. package/src/runtime/express/Express.js +2 -1
  73. package/src/runtime/lampp/Lampp.js +2 -2
  74. package/src/server/auth.js +3 -6
  75. package/src/server/data-query.js +449 -0
  76. package/src/server/dns.js +4 -4
  77. package/src/server/object-layer.js +0 -3
  78. package/src/ws/IoInterface.js +2 -2
@@ -7,7 +7,7 @@ import { loggerFactory } from '../../components/core/Logger.js';
7
7
  import { Modal } from '../../components/core/Modal.js';
8
8
  import { NotificationManager } from '../../components/core/NotificationManager.js';
9
9
  import { Translate } from '../../components/core/Translate.js';
10
- import { getQueryParams, RouterEvents, setQueryParams } from '../../components/core/Router.js';
10
+ import { getQueryParams, listenQueryParamsChange, RouterEvents, setQueryParams } from '../../components/core/Router.js';
11
11
  import { s } from '../../components/core/VanillaJs.js';
12
12
  import { DefaultService } from './default.service.js';
13
13
 
@@ -52,49 +52,136 @@ const columnDefFormatter = (obj, columnDefs, customFormat) => {
52
52
 
53
53
  const DefaultManagement = {
54
54
  Tokens: {},
55
- loadTable: async function (id, options = { reload: true, force: true }) {
56
- const { serviceId, columnDefs, customFormat } = this.Tokens[id];
57
-
58
- let _page = this.Tokens[id].page;
59
- let _limit = this.Tokens[id].limit;
60
- let _id = this.Tokens[id].serviceOptions?.get?.id ?? undefined;
61
- if (!options.force && this.Tokens[id].lastOptions) {
62
- if (
63
- _page === this.Tokens[id].lastOptions.page &&
64
- _limit === this.Tokens[id].lastOptions.limit &&
65
- _id === this.Tokens[id].lastOptions.id
66
- ) {
67
- logger.warn(`DefaultManagement loadTable ${serviceId} - Skipping load, options unchanged`);
68
- return;
55
+ loadTable: async function (id, options = { reload: true, force: true, createHistory: false, skipUrlUpdate: false }) {
56
+ try {
57
+ const { serviceId, columnDefs, customFormat, gridId } = this.Tokens[id];
58
+
59
+ let _page = this.Tokens[id].page;
60
+ let _limit = this.Tokens[id].limit;
61
+ let _id = this.Tokens[id].serviceOptions?.get?.id ?? undefined;
62
+
63
+ let filterModel = this.Tokens[id].filterModel || {};
64
+ let sortModel = this.Tokens[id].sortModel || [];
65
+
66
+ const gridApi = this.Tokens[id].gridApi || AgGrid.grids[gridId];
67
+
68
+ if (gridApi) {
69
+ filterModel = gridApi.getFilterModel() || {};
70
+ const columnState = gridApi.getColumnState();
71
+ if (columnState) {
72
+ sortModel = columnState
73
+ .filter((col) => col.sort)
74
+ .map((col) => ({ colId: col.colId, sort: col.sort, sortIndex: col.sortIndex }))
75
+ .sort((a, b) => (a.sortIndex || 0) - (b.sortIndex || 0));
76
+ }
69
77
  }
70
- }
71
- this.Tokens[id].lastOptions = {
72
- page: _page,
73
- limit: _limit,
74
- id: _id,
75
- };
76
78
 
77
- const result = await this.Tokens[id].ServiceProvider.get({
78
- page: _page,
79
- limit: _limit,
80
- id: _id,
81
- });
82
- if (result.status === 'success') {
83
- const { data, total, page, totalPages } = result.data;
84
- this.Tokens[id].total = total;
85
- this.Tokens[id].page = page;
86
- this.Tokens[id].totalPages = totalPages;
87
- const rowDataScope = data.map((row) => columnDefFormatter(row, columnDefs, customFormat));
88
- if (options.reload) AgGrid.grids[this.Tokens[id].gridId].setGridOption('rowData', rowDataScope);
89
- const paginationComp = s(`#ag-pagination-${this.Tokens[id].gridId}`);
90
- paginationComp.setAttribute('current-page', this.Tokens[id].page);
91
- paginationComp.setAttribute('total-pages', this.Tokens[id].totalPages);
92
- paginationComp.setAttribute('total-items', this.Tokens[id].total);
93
- setTimeout(async () => {
94
- if (DefaultManagement.Tokens[id].readyRowDataEvent)
95
- for (const event of Object.keys(DefaultManagement.Tokens[id].readyRowDataEvent))
96
- await DefaultManagement.Tokens[id].readyRowDataEvent[event](rowDataScope);
97
- }, 1);
79
+ // Clean up filterModel and sortModel for URL params
80
+ const filterModelStr = Object.keys(filterModel).length > 0 ? JSON.stringify(filterModel) : null;
81
+ const sortModelStr = sortModel.length > 0 ? JSON.stringify(sortModel) : null;
82
+
83
+ // Update URL parameters to reflect current grid state
84
+ // Use pushState (createHistory) for filter/sort changes to enable browser back/forward
85
+ // Skip URL update when handling browser navigation to avoid interfering with history
86
+ if (!options.skipUrlUpdate) {
87
+ setQueryParams(
88
+ {
89
+ page: _page,
90
+ limit: _limit,
91
+ filterModel: filterModelStr,
92
+ sortModel: sortModelStr,
93
+ },
94
+ { replace: !options.createHistory },
95
+ );
96
+ }
97
+
98
+ if (!options.force && this.Tokens[id].lastOptions) {
99
+ const last = this.Tokens[id].lastOptions;
100
+ if (
101
+ _page === last.page &&
102
+ _limit === last.limit &&
103
+ _id === last.id &&
104
+ JSON.stringify(filterModel) === JSON.stringify(last.filterModel) &&
105
+ JSON.stringify(sortModel) === JSON.stringify(last.sortModel)
106
+ ) {
107
+ logger.warn(`DefaultManagement loadTable ${serviceId} - Skipping load, options unchanged`);
108
+ return;
109
+ }
110
+ }
111
+ this.Tokens[id].lastOptions = {
112
+ page: _page,
113
+ limit: _limit,
114
+ id: _id,
115
+ filterModel,
116
+ sortModel,
117
+ };
118
+
119
+ // Update tokens with current state
120
+ this.Tokens[id].filterModel = filterModel;
121
+ this.Tokens[id].sortModel = sortModel;
122
+
123
+ const queryOptions = {
124
+ page: _page,
125
+ limit: _limit,
126
+ };
127
+
128
+ if (_id) {
129
+ queryOptions.id = _id;
130
+ }
131
+
132
+ if (filterModel && Object.keys(filterModel).length > 0) {
133
+ queryOptions.filterModel = filterModel;
134
+ }
135
+
136
+ if (sortModel && sortModel.length > 0) {
137
+ queryOptions.sortModel = sortModel;
138
+ // Legacy simple sort support
139
+ if (sortModel.length === 1) {
140
+ queryOptions.sort = sortModel[0].colId;
141
+ queryOptions.asc = sortModel[0].sort === 'asc' ? '1' : '0';
142
+ }
143
+ }
144
+
145
+ logger.info(`Loading table ${serviceId}`, {
146
+ page: _page,
147
+ limit: _limit,
148
+ hasFilters: Object.keys(filterModel).length > 0,
149
+ });
150
+
151
+ const result = await this.Tokens[id].ServiceProvider.get(queryOptions);
152
+ if (result.status === 'success') {
153
+ const { data, total, page, totalPages } = result.data;
154
+ this.Tokens[id].total = total;
155
+ this.Tokens[id].page = page;
156
+ this.Tokens[id].totalPages = totalPages;
157
+ const rowDataScope = data.map((row) => columnDefFormatter(row, columnDefs, customFormat));
158
+ if (options.reload) {
159
+ const grid = AgGrid.grids[this.Tokens[id].gridId];
160
+ if (grid && grid.setGridOption) {
161
+ grid.setGridOption('rowData', rowDataScope);
162
+ } else {
163
+ logger.warn(`Grid ${gridId} not found or not ready for setGridOption`);
164
+ }
165
+ }
166
+ const paginationComp = s(`#ag-pagination-${this.Tokens[id].gridId}`);
167
+ if (paginationComp) {
168
+ paginationComp.setAttribute('current-page', this.Tokens[id].page);
169
+ paginationComp.setAttribute('total-pages', this.Tokens[id].totalPages);
170
+ paginationComp.setAttribute('total-items', this.Tokens[id].total);
171
+ } else {
172
+ logger.warn(`Pagination component not found for grid ${gridId}`);
173
+ }
174
+ setTimeout(async () => {
175
+ if (DefaultManagement.Tokens[id].readyRowDataEvent)
176
+ for (const event of Object.keys(DefaultManagement.Tokens[id].readyRowDataEvent))
177
+ await DefaultManagement.Tokens[id].readyRowDataEvent[event](rowDataScope);
178
+ }, 1);
179
+ } else {
180
+ logger.error(`Failed to load table ${serviceId}:`, result);
181
+ }
182
+ } catch (error) {
183
+ logger.error(`Error in loadTable for ${id}:`, error);
184
+ throw error;
98
185
  }
99
186
  },
100
187
  refreshTable: async function (id) {
@@ -115,13 +202,100 @@ const DefaultManagement = {
115
202
  logger.info('DefaultManagement RenderTable', options);
116
203
  const id = options?.idModal ? options.idModal : getId(this.Tokens, `${serviceId}-`);
117
204
  const gridId = `${serviceId}-grid-${id}`;
205
+ const queryParamsListenerId = `default-management-${id}`;
118
206
  const queryParams = getQueryParams();
119
207
  const page = parseInt(queryParams.page) || 1;
120
208
  const defaultLimit = paginationOptions?.limitOptions?.[0] || 10;
121
209
  const limit = parseInt(queryParams.limit) || defaultLimit;
122
- this.Tokens[id] = { ...this.Tokens[id], ...options, gridId, page, limit, total: 0, totalPages: 1 };
123
210
 
124
- setQueryParams({ page, limit });
211
+ let filterModel = {};
212
+ let sortModel = [];
213
+ try {
214
+ if (queryParams.filterModel) filterModel = JSON.parse(queryParams.filterModel);
215
+ if (queryParams.sortModel) sortModel = JSON.parse(queryParams.sortModel);
216
+ } catch (e) {
217
+ logger.warn('Error parsing filter/sort model from URL', e);
218
+ }
219
+
220
+ // Enhance column definitions for Date filtering and ensure colId
221
+ const enhancedColumnDefs = columnDefs.map((col) => {
222
+ const enhancedCol = {
223
+ ...col,
224
+ colId: col.field, // Ensure colId matches field
225
+ };
226
+
227
+ if (enhancedCol.cellDataType === 'date' || enhancedCol.filter === 'agDateColumnFilter') {
228
+ enhancedCol.filter = 'agDateColumnFilter';
229
+
230
+ // Value getter to ensure date is properly parsed
231
+ if (!enhancedCol.valueGetter) {
232
+ enhancedCol.valueGetter = (params) => {
233
+ const value = params.data?.[enhancedCol.field];
234
+ if (!value) return null;
235
+ const date = new Date(value);
236
+ return isNaN(date.getTime()) ? null : date;
237
+ };
238
+ }
239
+
240
+ // Value formatter for display
241
+ if (!enhancedCol.valueFormatter) {
242
+ enhancedCol.valueFormatter = (params) => {
243
+ if (!params.value) return '';
244
+ const date = new Date(params.value);
245
+ if (isNaN(date.getTime())) return '';
246
+ return date.toLocaleDateString('en-US', {
247
+ year: 'numeric',
248
+ month: '2-digit',
249
+ day: '2-digit',
250
+ });
251
+ };
252
+ }
253
+
254
+ enhancedCol.filterParams = {
255
+ comparator: (filterLocalDateAtMidnight, cellValue) => {
256
+ if (cellValue == null) return -1;
257
+ const cellDate = new Date(cellValue);
258
+ if (isNaN(cellDate.getTime())) return -1;
259
+ // Compare dates (ignoring time)
260
+ const cellTime = new Date(cellDate).setHours(0, 0, 0, 0);
261
+ const filterTime = filterLocalDateAtMidnight.getTime();
262
+ if (filterTime === cellTime) return 0;
263
+ if (cellTime < filterTime) return -1;
264
+ if (cellTime > filterTime) return 1;
265
+ return 0;
266
+ },
267
+ browserDatePicker: true,
268
+ minValidYear: 2000,
269
+ maxValidYear: 2100,
270
+ inRangeInclusive: true,
271
+ debounceMs: 500,
272
+ ...enhancedCol.filterParams,
273
+ };
274
+ }
275
+ return enhancedCol;
276
+ });
277
+
278
+ this.Tokens[id] = {
279
+ ...this.Tokens[id],
280
+ ...options,
281
+ columnDefs: enhancedColumnDefs, // Use enhanced definitions
282
+ gridId,
283
+ page,
284
+ limit,
285
+ total: 0,
286
+ totalPages: 1,
287
+ filterModel,
288
+ sortModel,
289
+ isInitializing: true, // Flag to prevent double loading during grid ready
290
+ isProcessingQueryChange: false, // Flag to prevent listener recursion
291
+ };
292
+
293
+ setQueryParams({
294
+ page,
295
+ limit,
296
+ filterModel: Object.keys(filterModel).length > 0 ? JSON.stringify(filterModel) : null,
297
+ sortModel: sortModel.length > 0 ? JSON.stringify(sortModel) : null,
298
+ });
125
299
  setTimeout(async () => {
126
300
  // https://www.ag-grid.com/javascript-data-grid/data-update-transactions/
127
301
 
@@ -203,7 +377,7 @@ const DefaultManagement = {
203
377
 
204
378
  AgGrid.grids[gridId].setGridOption(
205
379
  'columnDefs',
206
- columnDefs.concat(
380
+ enhancedColumnDefs.concat(
207
381
  permissions.remove
208
382
  ? [
209
383
  {
@@ -217,7 +391,7 @@ const DefaultManagement = {
217
391
  : [],
218
392
  ),
219
393
  );
220
- DefaultManagement.loadTable(id);
394
+ // Initial loadTable is now called in onGridReady after grid is fully initialized
221
395
  // {
222
396
  // const result = await ServiceProvider.get();
223
397
  // if (result.status === 'success') {
@@ -348,6 +522,110 @@ const DefaultManagement = {
348
522
  DefaultManagement.loadTable(id);
349
523
  }
350
524
  });
525
+
526
+ // Listen to query parameter changes for browser back/forward navigation
527
+ listenQueryParamsChange({
528
+ id: queryParamsListenerId,
529
+ event: (queryParams) => {
530
+ // Prevent recursion - if we're already processing a query change, skip
531
+ if (this.Tokens[id].isProcessingQueryChange) {
532
+ return;
533
+ }
534
+
535
+ const newPage = parseInt(queryParams.page, 10) || 1;
536
+ const newLimit = parseInt(queryParams.limit, 10) || this.Tokens[id].limit || 10;
537
+ const newFilterModel = queryParams.filterModel;
538
+ const newSortModel = queryParams.sortModel;
539
+
540
+ let shouldReload = false;
541
+
542
+ // Check if page or limit changed
543
+ if (newPage !== this.Tokens[id].page || newLimit !== this.Tokens[id].limit) {
544
+ this.Tokens[id].page = newPage;
545
+ this.Tokens[id].limit = newLimit;
546
+ shouldReload = true;
547
+ }
548
+
549
+ // Check if filter or sort changed by comparing with actual grid state
550
+ const gridApi = AgGrid.grids[gridId];
551
+ let filterChanged = false;
552
+ let sortChanged = false;
553
+
554
+ if (gridApi) {
555
+ // Get current grid filter state
556
+ const currentGridFilterModel = gridApi.getFilterModel() || {};
557
+ const currentGridFilterStr = JSON.stringify(currentGridFilterModel);
558
+ const newFilterStr = newFilterModel || '{}';
559
+
560
+ // Get current grid sort state
561
+ const currentColumnState = gridApi.getColumnState() || [];
562
+ const currentGridSortModel = currentColumnState
563
+ .filter((col) => col.sort)
564
+ .map((col) => ({ colId: col.colId, sort: col.sort, sortIndex: col.sortIndex }))
565
+ .sort((a, b) => (a.sortIndex || 0) - (b.sortIndex || 0));
566
+ const currentGridSortStr = JSON.stringify(currentGridSortModel);
567
+ const newSortStr = newSortModel || '[]';
568
+
569
+ filterChanged = currentGridFilterStr !== newFilterStr;
570
+ sortChanged = currentGridSortStr !== newSortStr;
571
+ }
572
+
573
+ if (filterChanged || sortChanged) {
574
+ // Parse and apply the new filter/sort models
575
+ try {
576
+ this.Tokens[id].filterModel = newFilterModel ? JSON.parse(newFilterModel) : {};
577
+ } catch (e) {
578
+ this.Tokens[id].filterModel = {};
579
+ }
580
+ try {
581
+ this.Tokens[id].sortModel = newSortModel ? JSON.parse(newSortModel) : [];
582
+ } catch (e) {
583
+ this.Tokens[id].sortModel = [];
584
+ }
585
+
586
+ // Apply filters and sorts to the grid
587
+ if (gridApi) {
588
+ // Temporarily disable filter/sort change handlers to prevent recursion
589
+ this.Tokens[id].isProcessingQueryChange = true;
590
+
591
+ if (filterChanged) {
592
+ gridApi.setFilterModel(this.Tokens[id].filterModel);
593
+ }
594
+
595
+ if (sortChanged) {
596
+ // Apply sort model
597
+ const columnState = this.Tokens[id].sortModel.map((sortItem) => ({
598
+ colId: sortItem.colId,
599
+ sort: sortItem.sort,
600
+ sortIndex: sortItem.sortIndex,
601
+ }));
602
+ if (columnState.length > 0) {
603
+ gridApi.applyColumnState({
604
+ state: columnState,
605
+ defaultState: { sort: null },
606
+ });
607
+ } else {
608
+ gridApi.applyColumnState({
609
+ defaultState: { sort: null },
610
+ });
611
+ }
612
+ }
613
+
614
+ // Re-enable handlers after a short delay
615
+ setTimeout(() => {
616
+ this.Tokens[id].isProcessingQueryChange = false;
617
+ }, 100);
618
+ }
619
+ shouldReload = true;
620
+ }
621
+
622
+ if (shouldReload) {
623
+ // Skip URL update since browser already changed it (back/forward navigation)
624
+ DefaultManagement.loadTable(id, { reload: true, force: true, createHistory: false, skipUrlUpdate: true });
625
+ }
626
+ },
627
+ });
628
+
351
629
  EventsUI.onClick(`.management-table-btn-reload-${id}`, async () => {
352
630
  try {
353
631
  // Reload data from server
@@ -371,26 +649,75 @@ const DefaultManagement = {
371
649
  s(`#ag-pagination-${gridId}`).addEventListener('page-change', async (event) => {
372
650
  const token = DefaultManagement.Tokens[id];
373
651
  token.page = event.detail.page;
374
- await DefaultManagement.loadTable(id);
652
+ // Skip URL update since Pagination component already updated it
653
+ await DefaultManagement.loadTable(id, { skipUrlUpdate: true });
375
654
  });
376
655
  s(`#ag-pagination-${gridId}`).addEventListener('limit-change', async (event) => {
377
656
  const token = DefaultManagement.Tokens[id];
378
657
  token.limit = event.detail.limit;
379
658
  token.page = 1; // Reset to first page
380
- await DefaultManagement.loadTable(id);
659
+ // Skip URL update since Pagination component already updated it
660
+ await DefaultManagement.loadTable(id, { skipUrlUpdate: true });
381
661
  });
382
662
  RouterEvents[id] = async (...args) => {
383
663
  const queryParams = getQueryParams();
384
664
  const page = parseInt(queryParams.page) || 1;
385
665
  const limit = parseInt(queryParams.limit) || 10;
666
+ let filterModel = {};
667
+ let sortModel = [];
668
+ try {
669
+ if (queryParams.filterModel) filterModel = JSON.parse(queryParams.filterModel);
670
+ if (queryParams.sortModel) sortModel = JSON.parse(queryParams.sortModel);
671
+ } catch (e) {}
672
+
386
673
  const token = DefaultManagement.Tokens[id];
674
+ if (!token) {
675
+ // Token doesn't exist yet, table hasn't been initialized
676
+ return;
677
+ }
678
+
679
+ // Check if state in URL is different from current state
680
+ const isPaginationChanged = token.page !== page || token.limit !== limit;
681
+ const isFilterChanged = JSON.stringify(token.filterModel || {}) !== JSON.stringify(filterModel);
682
+ const isSortChanged = JSON.stringify(token.sortModel || []) !== JSON.stringify(sortModel);
387
683
 
388
- // Check if the pagination state in the URL is different from the current state
389
- if (token.page !== page || token.limit !== limit) {
684
+ if (isPaginationChanged || isFilterChanged || isSortChanged) {
390
685
  token.page = page;
391
686
  token.limit = limit;
392
- // Reload the table with the new pagination state from the URL
393
- await DefaultManagement.loadTable(id);
687
+ token.filterModel = filterModel;
688
+ token.sortModel = sortModel;
689
+
690
+ // If grid is active, we should update its state to match URL
691
+ const gridApi = AgGrid.grids[gridId];
692
+ if (gridApi && gridApi.setFilterModel && gridApi.applyColumnState) {
693
+ // Updating filter/sort on grid will trigger onFilterChanged/onSortChanged -> loadTable
694
+ // But we must ensure it happens.
695
+ if (isFilterChanged) {
696
+ try {
697
+ gridApi.setFilterModel(filterModel);
698
+ } catch (e) {
699
+ console.warn('Failed to set filter model:', e);
700
+ }
701
+ }
702
+ if (isSortChanged) {
703
+ try {
704
+ gridApi.applyColumnState({
705
+ state: sortModel,
706
+ defaultState: { sort: null },
707
+ });
708
+ } catch (e) {
709
+ console.warn('Failed to apply column state:', e);
710
+ }
711
+ }
712
+ // If only pagination changed, we must load manually because no grid event triggers
713
+ if (isPaginationChanged && !isFilterChanged && !isSortChanged) {
714
+ await DefaultManagement.loadTable(id);
715
+ }
716
+ } else if (!gridApi) {
717
+ // Grid doesn't exist yet, just load the table data when it's ready
718
+ // The onGridReady event will handle applying the initial state
719
+ logger.warn(`Grid not ready for ${gridId}, state will be applied on grid ready`);
720
+ }
394
721
  }
395
722
  };
396
723
  }, 1);
@@ -444,19 +771,69 @@ const DefaultManagement = {
444
771
  filter: true,
445
772
  autoHeight: true,
446
773
  },
447
- editType: 'fullRow',
774
+ onGridReady: (params) => {
775
+ this.Tokens[id].gridApi = params.api;
776
+ // Apply initial state from URL
777
+ const { filterModel, sortModel } = this.Tokens[id];
778
+ if (filterModel && Object.keys(filterModel).length > 0) {
779
+ params.api.setFilterModel(filterModel);
780
+ }
781
+ if (sortModel && sortModel.length > 0) {
782
+ params.api.applyColumnState({
783
+ state: sortModel,
784
+ defaultState: { sort: null },
785
+ });
786
+ }
787
+ // Load initial data now that grid is ready
788
+ // Filter/sort state has been applied, this will fetch data from server
789
+ DefaultManagement.loadTable(id).finally(() => {
790
+ // Mark initialization complete to allow future filter/sort events
791
+ this.Tokens[id].isInitializing = false;
792
+ });
793
+ },
794
+ onFilterChanged: () => {
795
+ // Skip if still initializing (state being applied in onGridReady)
796
+ if (this.Tokens[id].isInitializing) return;
797
+ // Skip if we're processing a query change from browser navigation
798
+ if (this.Tokens[id].isProcessingQueryChange) return;
799
+ // Reset to page 1 on filter change
800
+ this.Tokens[id].page = 1;
801
+ // Create history entry for filter changes
802
+ DefaultManagement.loadTable(id, { reload: true, force: true, createHistory: true });
803
+ },
804
+ onSortChanged: () => {
805
+ // Skip if still initializing (state being applied in onGridReady)
806
+ if (this.Tokens[id].isInitializing) return;
807
+ // Skip if we're processing a query change from browser navigation
808
+ if (this.Tokens[id].isProcessingQueryChange) return;
809
+ // Create history entry for sort changes
810
+ DefaultManagement.loadTable(id, { reload: true, force: true, createHistory: true });
811
+ },
812
+ editType: 'fullRow', // Keep fullRow for add new row, but cells will auto-save
448
813
  // rowData: [],
449
814
  onCellValueChanged: async (...args) => {
450
- console.log('onCellValueChanged', args);
451
- // field: event.colDef.field,
452
- // body[event.colDef.field] = event.newValue;
453
- // NotificationManager.Push({
454
- // html:
455
- // result.status === 'error'
456
- // ? result.message
457
- // : `${Translate.Render('field')} ${event.colDef.headerName} ${Translate.Render('success-updated')}`,
458
- // status: result.status,
459
- // });
815
+ const [event] = args;
816
+ // Only auto-save for existing rows (with _id), not new rows
817
+ if (event.data && event.data._id) {
818
+ logger.info('onCellValueChanged - auto-saving', args);
819
+ const body = event.data ? event.data : {};
820
+ const result = await ServiceProvider.put({ id: event.data._id, body });
821
+ NotificationManager.Push({
822
+ html: result.status === 'error' ? result.message : `${Translate.Render('success-update-item')}`,
823
+ status: result.status,
824
+ });
825
+ if (result.status === 'success') {
826
+ AgGrid.grids[gridId].applyTransaction({
827
+ update: [event.data],
828
+ });
829
+ // Restore default buttons after save
830
+ s(`.management-table-btn-save-${id}`).classList.add('hide');
831
+ if (permissions.add) s(`.management-table-btn-add-${id}`).classList.remove('hide');
832
+ if (permissions.remove) s(`.management-table-btn-clean-${id}`).classList.remove('hide');
833
+ if (permissions.reload) s(`.management-table-btn-reload-${id}`).classList.remove('hide');
834
+ DefaultManagement.loadTable(id, { reload: false });
835
+ }
836
+ }
460
837
  },
461
838
  rowSelection: 'single',
462
839
  onSelectionChanged: async (...args) => {
@@ -465,8 +842,18 @@ const DefaultManagement = {
465
842
  const selectedRows = AgGrid.grids[gridId].getSelectedRows();
466
843
  logger.info('selectedRows', selectedRows);
467
844
  },
845
+ onCellEditingStarted: async (...args) => {
846
+ const [event] = args;
847
+ // Only show save button for new rows (without _id)
848
+ if (event.data && !event.data._id) {
849
+ s(`.management-table-btn-save-${id}`).classList.remove('hide');
850
+ if (permissions.add) s(`.management-table-btn-add-${id}`).classList.add('hide');
851
+ if (permissions.remove) s(`.management-table-btn-clean-${id}`).classList.add('hide');
852
+ if (permissions.reload) s(`.management-table-btn-reload-${id}`).classList.add('hide');
853
+ }
854
+ },
468
855
  onRowEditingStarted: async (...args) => {
469
- // Show only save button when editing starts
856
+ // Show only save button when editing starts (for new rows)
470
857
  s(`.management-table-btn-save-${id}`).classList.remove('hide');
471
858
  if (permissions.add) s(`.management-table-btn-add-${id}`).classList.add('hide');
472
859
  if (permissions.remove) s(`.management-table-btn-clean-${id}`).classList.add('hide');
@@ -1,6 +1,6 @@
1
1
  import { Auth } from '../../components/core/Auth.js';
2
2
  import { loggerFactory } from '../../components/core/Logger.js';
3
- import { getApiBaseUrl, headersFactory, payloadFactory } from '../core/core.service.js';
3
+ import { getApiBaseUrl, headersFactory, payloadFactory, buildQueryUrl } from '../core/core.service.js';
4
4
 
5
5
  const logger = loggerFactory(import.meta);
6
6
 
@@ -49,11 +49,18 @@ const DefaultService = {
49
49
  return reject(error);
50
50
  }),
51
51
  ),
52
- get: (options = { id: '', body: {}, page: 1, limit: 10 }) => {
53
- const { id, page, limit } = options;
54
- const url = new URL(getApiBaseUrl({ id, endpoint }));
55
- url.searchParams.append('page', page);
56
- url.searchParams.append('limit', limit);
52
+ get: (options = {}) => {
53
+ const { id, page, limit, filterModel, sortModel, sort, asc, order } = options;
54
+ const url = buildQueryUrl(getApiBaseUrl({ id, endpoint }), {
55
+ page,
56
+ limit,
57
+ filterModel,
58
+ sortModel,
59
+ sort,
60
+ asc,
61
+ order,
62
+ });
63
+
57
64
  return new Promise((resolve, reject) =>
58
65
  fetch(url.toString(), {
59
66
  method: 'GET',
@@ -111,6 +111,29 @@ const DocumentService = {
111
111
  return reject(error);
112
112
  }),
113
113
  ),
114
+ high: (options = { params: {} }) =>
115
+ new Promise((resolve, reject) => {
116
+ const url = new URL(getApiBaseUrl({ id: 'public/high', endpoint }));
117
+ if (options.params) {
118
+ Object.keys(options.params).forEach((key) => url.searchParams.append(key, options.params[key]));
119
+ }
120
+ fetch(url, {
121
+ method: 'GET',
122
+ headers: headersFactory(),
123
+ credentials: 'include',
124
+ })
125
+ .then(async (res) => {
126
+ return await res.json();
127
+ })
128
+ .then((res) => {
129
+ logger.info(res);
130
+ return resolve(res);
131
+ })
132
+ .catch((error) => {
133
+ logger.error(error);
134
+ return reject(error);
135
+ });
136
+ }),
114
137
  };
115
138
 
116
139
  export { DocumentService };