@underpostnet/underpost 2.97.1 → 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.
- package/README.md +2 -2
- package/cli.md +3 -1
- package/conf.js +2 -0
- package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
- package/manifests/deployment/dd-test-development/deployment.yaml +2 -2
- package/package.json +1 -1
- package/src/api/core/core.service.js +0 -5
- package/src/api/default/default.service.js +7 -5
- package/src/api/document/document.model.js +1 -1
- package/src/api/document/document.router.js +5 -0
- package/src/api/document/document.service.js +105 -47
- package/src/api/file/file.model.js +112 -4
- package/src/api/file/file.ref.json +42 -0
- package/src/api/file/file.service.js +380 -32
- package/src/api/user/user.model.js +38 -1
- package/src/api/user/user.router.js +96 -63
- package/src/api/user/user.service.js +81 -48
- package/src/cli/db.js +424 -166
- package/src/cli/index.js +8 -0
- package/src/cli/repository.js +1 -1
- package/src/cli/run.js +1 -0
- package/src/cli/ssh.js +10 -10
- package/src/client/components/core/Account.js +327 -36
- package/src/client/components/core/AgGrid.js +3 -0
- package/src/client/components/core/Auth.js +9 -3
- package/src/client/components/core/Chat.js +2 -2
- package/src/client/components/core/Content.js +159 -78
- package/src/client/components/core/CssCore.js +16 -12
- package/src/client/components/core/FileExplorer.js +115 -8
- package/src/client/components/core/Input.js +204 -11
- package/src/client/components/core/LogIn.js +42 -20
- package/src/client/components/core/Modal.js +138 -24
- package/src/client/components/core/Panel.js +69 -31
- package/src/client/components/core/PanelForm.js +262 -77
- package/src/client/components/core/PublicProfile.js +888 -0
- package/src/client/components/core/Router.js +117 -15
- package/src/client/components/core/SearchBox.js +329 -13
- package/src/client/components/core/SignUp.js +26 -7
- package/src/client/components/core/SocketIo.js +6 -3
- package/src/client/components/core/Translate.js +98 -0
- package/src/client/components/core/Validator.js +15 -0
- package/src/client/components/core/windowGetDimensions.js +6 -6
- package/src/client/components/default/MenuDefault.js +59 -12
- package/src/client/components/default/RoutesDefault.js +1 -0
- package/src/client/services/core/core.service.js +163 -1
- package/src/client/services/default/default.management.js +451 -64
- package/src/client/services/default/default.service.js +13 -6
- package/src/client/services/file/file.service.js +43 -16
- package/src/client/services/user/user.service.js +13 -9
- package/src/db/DataBaseProvider.js +1 -1
- package/src/db/mongo/MongooseDB.js +1 -1
- package/src/index.js +1 -1
- package/src/mailer/MailerProvider.js +4 -4
- package/src/runtime/express/Express.js +2 -1
- package/src/runtime/lampp/Lampp.js +2 -2
- package/src/server/auth.js +3 -6
- package/src/server/data-query.js +449 -0
- package/src/server/object-layer.js +0 -3
- package/src/ws/IoInterface.js +2 -2
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Input component module for form controls and file handling utilities.
|
|
3
|
+
* Provides input rendering, file data conversion, and blob endpoint integration.
|
|
4
|
+
*
|
|
5
|
+
* @module src/client/components/core/Input.js
|
|
6
|
+
* @namespace InputClient
|
|
7
|
+
*/
|
|
8
|
+
|
|
1
9
|
import { AgGrid } from './AgGrid.js';
|
|
2
10
|
import { BtnIcon } from './BtnIcon.js';
|
|
3
11
|
import { isValidDate } from './CommonJs.js';
|
|
@@ -8,8 +16,26 @@ import { RichText } from './RichText.js';
|
|
|
8
16
|
import { ToggleSwitch } from './ToggleSwitch.js';
|
|
9
17
|
import { Translate } from './Translate.js';
|
|
10
18
|
import { htmls, htmlStrSanitize, s } from './VanillaJs.js';
|
|
19
|
+
import { getApiBaseUrl } from '../../services/core/core.service.js';
|
|
20
|
+
import { FileService } from '../../services/file/file.service.js';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Logger instance for this module.
|
|
24
|
+
* @type {Function}
|
|
25
|
+
* @memberof InputClient
|
|
26
|
+
* @private
|
|
27
|
+
*/
|
|
11
28
|
const logger = loggerFactory(import.meta);
|
|
12
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Creates a FormData object from file input event.
|
|
32
|
+
* Filters files by extension if provided.
|
|
33
|
+
* @function fileFormDataFactory
|
|
34
|
+
* @memberof InputClient
|
|
35
|
+
* @param {Event} e - The input change event containing files.
|
|
36
|
+
* @param {string[]} [extensions] - Optional array of allowed MIME types.
|
|
37
|
+
* @returns {FormData} FormData object containing the valid files.
|
|
38
|
+
*/
|
|
13
39
|
const fileFormDataFactory = (e, extensions) => {
|
|
14
40
|
const form = new FormData();
|
|
15
41
|
for (const keyFile of Object.keys(e.target.files)) {
|
|
@@ -22,17 +48,156 @@ const fileFormDataFactory = (e, extensions) => {
|
|
|
22
48
|
return form;
|
|
23
49
|
};
|
|
24
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Convert file data to File object.
|
|
53
|
+
* Supports both legacy format (with buffer data) and new format (metadata only).
|
|
54
|
+
*
|
|
55
|
+
* Legacy format: `{ data: { data: [0, 1, 2, ...] }, mimetype: 'text/markdown', name: 'file.md' }`
|
|
56
|
+
* New format: `{ _id: '...', mimetype: 'text/markdown', name: 'file.md' }`
|
|
57
|
+
*
|
|
58
|
+
* @function getFileFromFileData
|
|
59
|
+
* @memberof InputClient
|
|
60
|
+
* @param {Object} fileData - File data object in legacy or new format.
|
|
61
|
+
* @param {Object} [fileData.data] - Legacy format data container.
|
|
62
|
+
* @param {Array<number>} [fileData.data.data] - Legacy format byte array.
|
|
63
|
+
* @param {string} [fileData._id] - New format file ID.
|
|
64
|
+
* @param {string} fileData.mimetype - MIME type of the file.
|
|
65
|
+
* @param {string} fileData.name - Name of the file.
|
|
66
|
+
* @returns {File|null} File object if legacy format, null if metadata-only or invalid.
|
|
67
|
+
*/
|
|
25
68
|
const getFileFromFileData = (fileData) => {
|
|
26
|
-
|
|
27
|
-
|
|
69
|
+
if (!fileData) {
|
|
70
|
+
logger.error('getFileFromFileData: fileData is undefined');
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Check if this is legacy format with buffer data
|
|
75
|
+
if (fileData.data?.data) {
|
|
76
|
+
try {
|
|
77
|
+
const blob = new Blob([new Uint8Array(fileData.data.data)], { type: fileData.mimetype });
|
|
78
|
+
return new File([blob], fileData.name, { type: fileData.mimetype });
|
|
79
|
+
} catch (error) {
|
|
80
|
+
logger.error('Error creating File from legacy buffer data:', error);
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// New format - metadata only, cannot create File without content
|
|
86
|
+
// Return null and let caller fetch from blob endpoint if needed
|
|
87
|
+
if (fileData._id && !fileData.data?.data) {
|
|
88
|
+
logger.warn(
|
|
89
|
+
'getFileFromFileData: File is metadata-only, cannot create File object without content. File ID:',
|
|
90
|
+
fileData._id,
|
|
91
|
+
);
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
logger.error('getFileFromFileData: Invalid file data structure', fileData);
|
|
96
|
+
return null;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Fetch file content from blob endpoint and create File object.
|
|
101
|
+
* Used for metadata-only format files during edit mode.
|
|
102
|
+
* Uses FileService with blob/ prefix for centralized blob fetching.
|
|
103
|
+
*
|
|
104
|
+
* @async
|
|
105
|
+
* @function getFileFromBlobEndpoint
|
|
106
|
+
* @memberof InputClient
|
|
107
|
+
* @param {Object} fileData - File metadata object with _id.
|
|
108
|
+
* @param {string} fileData._id - File ID for blob endpoint lookup.
|
|
109
|
+
* @param {string} [fileData.name] - Optional file name.
|
|
110
|
+
* @param {string} [fileData.mimetype] - Optional MIME type.
|
|
111
|
+
* @returns {Promise<File|null>} File object from blob endpoint, or null on error.
|
|
112
|
+
*/
|
|
113
|
+
const getFileFromBlobEndpoint = async (fileData) => {
|
|
114
|
+
if (!fileData || !fileData._id) {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
const { data: blobArray, status } = await FileService.get({ id: `blob/${fileData._id}` });
|
|
120
|
+
if (status !== 'success' || !blobArray || !blobArray[0]) {
|
|
121
|
+
logger.error('Failed to fetch file from blob endpoint');
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const blob = blobArray[0];
|
|
126
|
+
return new File([blob], fileData.name || 'file', { type: fileData.mimetype || blob.type });
|
|
127
|
+
} catch (error) {
|
|
128
|
+
logger.error('Error fetching file from blob endpoint:', error);
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
28
131
|
};
|
|
29
132
|
|
|
133
|
+
/**
|
|
134
|
+
* Get image/file source URL from file data.
|
|
135
|
+
* Supports both legacy format (with buffer) and new format (metadata only).
|
|
136
|
+
* For new format, returns blob endpoint URL.
|
|
137
|
+
*
|
|
138
|
+
* @function getSrcFromFileData
|
|
139
|
+
* @memberof InputClient
|
|
140
|
+
* @param {Object} fileData - File data object in legacy or new format.
|
|
141
|
+
* @param {Object} [fileData.data] - Legacy format data container.
|
|
142
|
+
* @param {Array<number>} [fileData.data.data] - Legacy format byte array.
|
|
143
|
+
* @param {string} [fileData._id] - New format file ID for blob endpoint.
|
|
144
|
+
* @param {string} fileData.mimetype - MIME type of the file.
|
|
145
|
+
* @returns {string|null} Object URL for legacy format, blob endpoint URL for new format, or null on error.
|
|
146
|
+
*/
|
|
30
147
|
const getSrcFromFileData = (fileData) => {
|
|
31
|
-
|
|
32
|
-
|
|
148
|
+
if (!fileData) {
|
|
149
|
+
logger.error('getSrcFromFileData: fileData is undefined');
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Legacy format with buffer data - create object URL
|
|
154
|
+
if (fileData.data?.data) {
|
|
155
|
+
try {
|
|
156
|
+
const file = getFileFromFileData(fileData);
|
|
157
|
+
if (file) {
|
|
158
|
+
return URL.createObjectURL(file);
|
|
159
|
+
}
|
|
160
|
+
} catch (error) {
|
|
161
|
+
logger.error('Error getting src from legacy buffer data:', error);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// New format - use blob endpoint
|
|
166
|
+
if (fileData._id) {
|
|
167
|
+
try {
|
|
168
|
+
return getApiBaseUrl({ id: fileData._id, endpoint: 'file/blob' });
|
|
169
|
+
} catch (error) {
|
|
170
|
+
logger.error('Error generating blob URL:', error);
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
logger.error('getSrcFromFileData: Cannot generate src, invalid file data:', fileData);
|
|
176
|
+
return null;
|
|
33
177
|
};
|
|
34
178
|
|
|
179
|
+
/**
|
|
180
|
+
* Input component for rendering various form input types.
|
|
181
|
+
* Supports text, password, file, color, date, dropdown, toggle, rich text, and grid inputs.
|
|
182
|
+
* @namespace InputClient.Input
|
|
183
|
+
* @memberof InputClient
|
|
184
|
+
*/
|
|
35
185
|
const Input = {
|
|
186
|
+
/**
|
|
187
|
+
* Renders an input element based on the provided options.
|
|
188
|
+
* @async
|
|
189
|
+
* @function Render
|
|
190
|
+
* @memberof InputClient.Input
|
|
191
|
+
* @param {Object} options - Input configuration options.
|
|
192
|
+
* @param {string} options.id - Unique identifier for the input.
|
|
193
|
+
* @param {string} [options.type] - Input type (text, password, file, color, datetime-local, etc.).
|
|
194
|
+
* @param {string} [options.placeholder] - Placeholder text.
|
|
195
|
+
* @param {string} [options.label] - Label text for the input.
|
|
196
|
+
* @param {string} [options.containerClass] - CSS class for the container.
|
|
197
|
+
* @param {string} [options.inputClass] - CSS class for the input element.
|
|
198
|
+
* @param {boolean} [options.disabled] - Whether the input is disabled.
|
|
199
|
+
* @returns {Promise<string>} HTML string for the input component.
|
|
200
|
+
*/
|
|
36
201
|
Render: async function (options) {
|
|
37
202
|
const { id } = options;
|
|
38
203
|
options?.placeholder
|
|
@@ -184,8 +349,8 @@ const Input = {
|
|
|
184
349
|
}
|
|
185
350
|
return obj;
|
|
186
351
|
},
|
|
187
|
-
setValues: function (formData, obj, originObj, fileObj) {
|
|
188
|
-
setTimeout(() => {
|
|
352
|
+
setValues: async function (formData, obj, originObj, fileObj) {
|
|
353
|
+
setTimeout(async () => {
|
|
189
354
|
for (const inputData of formData) {
|
|
190
355
|
if (!s(`.${inputData.id}`)) continue;
|
|
191
356
|
|
|
@@ -194,11 +359,31 @@ const Input = {
|
|
|
194
359
|
if (fileObj && fileObj[inputData.model] && s(`.${inputData.id}`)) {
|
|
195
360
|
const dataTransfer = new DataTransfer();
|
|
196
361
|
|
|
197
|
-
if (fileObj[inputData.model].fileBlob)
|
|
198
|
-
|
|
362
|
+
if (fileObj[inputData.model].fileBlob) {
|
|
363
|
+
let fileBlobData = getFileFromFileData(fileObj[inputData.model].fileBlob);
|
|
364
|
+
|
|
365
|
+
// If fileBlob is metadata-only, try to fetch from blob endpoint
|
|
366
|
+
if (!fileBlobData && fileObj[inputData.model].fileBlob?._id) {
|
|
367
|
+
fileBlobData = await getFileFromBlobEndpoint(fileObj[inputData.model].fileBlob);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
if (fileBlobData) {
|
|
371
|
+
dataTransfer.items.add(fileBlobData);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
199
374
|
|
|
200
|
-
if (fileObj[inputData.model].mdBlob)
|
|
201
|
-
|
|
375
|
+
if (fileObj[inputData.model].mdBlob) {
|
|
376
|
+
let mdBlobData = getFileFromFileData(fileObj[inputData.model].mdBlob);
|
|
377
|
+
|
|
378
|
+
// If mdBlob is metadata-only, try to fetch from blob endpoint
|
|
379
|
+
if (!mdBlobData && fileObj[inputData.model].mdBlob?._id) {
|
|
380
|
+
mdBlobData = await getFileFromBlobEndpoint(fileObj[inputData.model].mdBlob);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (mdBlobData) {
|
|
384
|
+
dataTransfer.items.add(mdBlobData);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
202
387
|
|
|
203
388
|
if (dataTransfer.files.length) {
|
|
204
389
|
s(`.${inputData.id}`).files = dataTransfer.files;
|
|
@@ -384,4 +569,12 @@ function isTextInputFocused() {
|
|
|
384
569
|
return active && (active.tagName === 'INPUT' || active.tagName === 'TEXTAREA');
|
|
385
570
|
}
|
|
386
571
|
|
|
387
|
-
export {
|
|
572
|
+
export {
|
|
573
|
+
Input,
|
|
574
|
+
InputFile,
|
|
575
|
+
fileFormDataFactory,
|
|
576
|
+
getSrcFromFileData,
|
|
577
|
+
getFileFromFileData,
|
|
578
|
+
getFileFromBlobEndpoint,
|
|
579
|
+
isTextInputFocused,
|
|
580
|
+
};
|
|
@@ -1,16 +1,19 @@
|
|
|
1
|
-
import { CoreService } from '../../services/core/core.service.js';
|
|
1
|
+
import { CoreService, getApiBaseUrl } from '../../services/core/core.service.js';
|
|
2
2
|
import { FileService } from '../../services/file/file.service.js';
|
|
3
3
|
import { UserService } from '../../services/user/user.service.js';
|
|
4
4
|
import { Auth } from './Auth.js';
|
|
5
5
|
import { BtnIcon } from './BtnIcon.js';
|
|
6
6
|
import { EventsUI } from './EventsUI.js';
|
|
7
7
|
import { Input } from './Input.js';
|
|
8
|
+
import { loggerFactory } from './Logger.js';
|
|
8
9
|
import { NotificationManager } from './NotificationManager.js';
|
|
9
10
|
import { Translate } from './Translate.js';
|
|
10
11
|
import { Validator } from './Validator.js';
|
|
11
12
|
import { htmls, s } from './VanillaJs.js';
|
|
12
13
|
import { Webhook } from './Webhook.js';
|
|
13
14
|
|
|
15
|
+
const logger = loggerFactory(import.meta);
|
|
16
|
+
|
|
14
17
|
const LogIn = {
|
|
15
18
|
Scope: {
|
|
16
19
|
user: {
|
|
@@ -24,6 +27,8 @@ const LogIn = {
|
|
|
24
27
|
Event: {},
|
|
25
28
|
Trigger: async function (options) {
|
|
26
29
|
const { user } = options;
|
|
30
|
+
if (user) this.Scope.user.main.model.user = { ...this.Scope.user.main.model.user, ...user };
|
|
31
|
+
|
|
27
32
|
for (const eventKey of Object.keys(this.Event)) await this.Event[eventKey](options);
|
|
28
33
|
if (!user || user.role === 'guest') return;
|
|
29
34
|
await Webhook.register({ user });
|
|
@@ -52,26 +57,40 @@ const LogIn = {
|
|
|
52
57
|
</style>`,
|
|
53
58
|
);
|
|
54
59
|
if (!this.Scope.user.main.model.user.profileImage) {
|
|
55
|
-
|
|
56
|
-
if (
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
60
|
+
// Try to load profile image only if profileImageId exists
|
|
61
|
+
if (!this.Scope.user.main.model.user.profileImage && user?.profileImageId) {
|
|
62
|
+
try {
|
|
63
|
+
const resultFile = await FileService.get({ id: user.profileImageId });
|
|
64
|
+
if (resultFile && resultFile.status === 'success' && resultFile.data[0]) {
|
|
65
|
+
const imageData = resultFile.data[0];
|
|
66
|
+
let imageSrc = null;
|
|
62
67
|
|
|
63
|
-
|
|
68
|
+
try {
|
|
69
|
+
// Handle new metadata-only format
|
|
70
|
+
if (!imageData.data?.data && imageData._id) {
|
|
71
|
+
imageSrc = getApiBaseUrl({ id: imageData._id, endpoint: 'file/blob' });
|
|
72
|
+
}
|
|
73
|
+
// Handle legacy format with buffer data
|
|
74
|
+
else if (imageData.data?.data) {
|
|
75
|
+
const imageBlob = new Blob([new Uint8Array(imageData.data.data)], { type: imageData.mimetype });
|
|
76
|
+
const imageFile = new File([imageBlob], imageData.name, { type: imageData.mimetype });
|
|
77
|
+
imageSrc = URL.createObjectURL(imageFile);
|
|
78
|
+
}
|
|
64
79
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
80
|
+
if (imageSrc) {
|
|
81
|
+
this.Scope.user.main.model.user.profileImage = {
|
|
82
|
+
resultFile,
|
|
83
|
+
imageData,
|
|
84
|
+
imageSrc,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
} catch (error) {
|
|
88
|
+
logger.warn('Error processing profile image:', error);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
} catch (error) {
|
|
92
|
+
logger.warn('Error fetching profile image:', error);
|
|
93
|
+
}
|
|
75
94
|
}
|
|
76
95
|
htmls(
|
|
77
96
|
`.action-btn-profile-log-in-render`,
|
|
@@ -80,7 +99,10 @@ const LogIn = {
|
|
|
80
99
|
class="abs center top-box-profile-img"
|
|
81
100
|
${this.Scope.user.main.model.user.profileImage
|
|
82
101
|
? `src="${this.Scope.user.main.model.user.profileImage.imageSrc}"`
|
|
83
|
-
:
|
|
102
|
+
: `src="${getApiBaseUrl({
|
|
103
|
+
id: 'assets/avatar',
|
|
104
|
+
endpoint: 'user',
|
|
105
|
+
})}"`}
|
|
84
106
|
/>
|
|
85
107
|
</div>`,
|
|
86
108
|
);
|
|
@@ -74,7 +74,7 @@ const Modal = {
|
|
|
74
74
|
const minWidth = width;
|
|
75
75
|
const heightDefaultTopBar = 50;
|
|
76
76
|
const heightDefaultBottomBar = 0;
|
|
77
|
-
const idModal = options
|
|
77
|
+
const idModal = options.id ? options.id : getId(this.Data, 'modal-');
|
|
78
78
|
this.Data[idModal] = {
|
|
79
79
|
options,
|
|
80
80
|
onCloseListener: {},
|
|
@@ -572,7 +572,6 @@ const Modal = {
|
|
|
572
572
|
});
|
|
573
573
|
let currentKeyBoardSearchBoxIndex = 0;
|
|
574
574
|
let results = [];
|
|
575
|
-
let historySearchBox = [];
|
|
576
575
|
|
|
577
576
|
const checkHistoryBoxTitleStatus = () => {
|
|
578
577
|
if (s(`.search-box-result-title`) && s(`.search-box-result-title`).classList) {
|
|
@@ -593,9 +592,28 @@ const Modal = {
|
|
|
593
592
|
} else s(`.key-shortcut-container-info`).classList.add('hide');
|
|
594
593
|
};
|
|
595
594
|
|
|
596
|
-
const renderSearchResult = async (results) => {
|
|
595
|
+
const renderSearchResult = async (results, isRecentHistory = false) => {
|
|
596
|
+
// Check if the search history modal still exists before rendering
|
|
597
|
+
if (!s(`.html-${searchBoxHistoryId}`)) return;
|
|
598
|
+
|
|
597
599
|
htmls(`.html-${searchBoxHistoryId}`, '');
|
|
598
600
|
|
|
601
|
+
// Show/hide clear-all button based on whether showing recent history
|
|
602
|
+
// Use setTimeout to ensure the button is in the DOM after modal renders
|
|
603
|
+
const updateClearAllBtn = () => {
|
|
604
|
+
const clearAllBtn = s(`.btn-search-history-clear-all`);
|
|
605
|
+
if (clearAllBtn) {
|
|
606
|
+
if (isRecentHistory && results.length > 0) {
|
|
607
|
+
clearAllBtn.style.display = 'flex';
|
|
608
|
+
} else {
|
|
609
|
+
clearAllBtn.style.display = 'none';
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
};
|
|
613
|
+
// Try immediately and also with a delay to handle timing
|
|
614
|
+
updateClearAllBtn();
|
|
615
|
+
setTimeout(updateClearAllBtn, 50);
|
|
616
|
+
|
|
599
617
|
if (results.length === 0) {
|
|
600
618
|
append(
|
|
601
619
|
`.html-${searchBoxHistoryId}`,
|
|
@@ -621,6 +639,7 @@ const Modal = {
|
|
|
621
639
|
const searchContext = {
|
|
622
640
|
RouterInstance: Worker.RouterInstance,
|
|
623
641
|
options: options,
|
|
642
|
+
isRecentHistory: isRecentHistory, // Flag for delete button visibility
|
|
624
643
|
onResultClick: () => {
|
|
625
644
|
// Dismiss search box on result click
|
|
626
645
|
if (s(`.${searchBoxHistoryId}`)) {
|
|
@@ -662,13 +681,14 @@ const Modal = {
|
|
|
662
681
|
const minLength = searchContext.minQueryLength;
|
|
663
682
|
if (trimmedQuery.length >= minLength) {
|
|
664
683
|
results = await SearchBox.search(trimmedQuery, searchContext);
|
|
665
|
-
renderSearchResult(results);
|
|
684
|
+
renderSearchResult(results, false); // Search results - no delete buttons
|
|
666
685
|
} else if (trimmedQuery.length === 0) {
|
|
667
|
-
// Show history when query is empty
|
|
668
|
-
|
|
686
|
+
// Show recent results from persistent history when query is empty
|
|
687
|
+
const recentResults = SearchBox.RecentResults.getAll();
|
|
688
|
+
renderSearchResult(recentResults, true); // Recent history - show delete buttons
|
|
669
689
|
} else {
|
|
670
690
|
// Query is too short - show nothing or a hint
|
|
671
|
-
renderSearchResult([]);
|
|
691
|
+
renderSearchResult([], false);
|
|
672
692
|
}
|
|
673
693
|
}
|
|
674
694
|
break;
|
|
@@ -698,8 +718,6 @@ const Modal = {
|
|
|
698
718
|
});
|
|
699
719
|
};
|
|
700
720
|
|
|
701
|
-
const getDefaultSearchBoxSelector = () => `.search-result-btn-${currentKeyBoardSearchBoxIndex}`;
|
|
702
|
-
|
|
703
721
|
const updateSearchBoxValue = (selector) => {
|
|
704
722
|
if (!selector) {
|
|
705
723
|
// Get the currently active search result item
|
|
@@ -732,15 +750,18 @@ const Modal = {
|
|
|
732
750
|
const resultType = activeItem.getAttribute('data-result-type');
|
|
733
751
|
|
|
734
752
|
if (resultType === 'route' && results[currentKeyBoardSearchBoxIndex]) {
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
);
|
|
738
|
-
historySearchBox.unshift(results[currentKeyBoardSearchBoxIndex]);
|
|
753
|
+
const result = results[currentKeyBoardSearchBoxIndex];
|
|
754
|
+
// Track in persistent history
|
|
755
|
+
SearchBox.RecentResults.add(result);
|
|
739
756
|
updateSearchBoxValue();
|
|
740
757
|
if (s(`.main-btn-${resultId}`)) {
|
|
741
758
|
s(`.main-btn-${resultId}`).click();
|
|
742
759
|
}
|
|
743
760
|
} else {
|
|
761
|
+
// Track custom provider result in persistent history
|
|
762
|
+
if (results[currentKeyBoardSearchBoxIndex]) {
|
|
763
|
+
SearchBox.RecentResults.add(results[currentKeyBoardSearchBoxIndex]);
|
|
764
|
+
}
|
|
744
765
|
// Trigger click on custom result
|
|
745
766
|
activeItem.click();
|
|
746
767
|
}
|
|
@@ -761,21 +782,28 @@ const Modal = {
|
|
|
761
782
|
barConfig.buttons.restore.disabled = true;
|
|
762
783
|
barConfig.buttons.menu.disabled = true;
|
|
763
784
|
barConfig.buttons.close.disabled = false;
|
|
785
|
+
|
|
764
786
|
await Modal.Render({
|
|
765
787
|
id: searchBoxHistoryId,
|
|
766
788
|
barConfig,
|
|
767
|
-
title: html`<div
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
789
|
+
title: html`<div
|
|
790
|
+
style="display: flex; align-items: center; justify-content: space-between; width: 100%;"
|
|
791
|
+
>
|
|
792
|
+
<div style="display: flex; align-items: center; gap: 8px;">
|
|
793
|
+
<div class="search-box-recent-title">
|
|
794
|
+
${renderViewTitle({
|
|
795
|
+
icon: html`<i class="fas fa-history mini-title"></i>`,
|
|
796
|
+
text: Translate.Render('recent'),
|
|
797
|
+
})}
|
|
798
|
+
</div>
|
|
799
|
+
<div class="search-box-result-title hide">
|
|
800
|
+
${renderViewTitle({
|
|
801
|
+
icon: html`<i class="far fa-list-alt mini-title"></i>`,
|
|
802
|
+
text: Translate.Render('results'),
|
|
803
|
+
})}
|
|
804
|
+
</div>
|
|
772
805
|
</div>
|
|
773
|
-
|
|
774
|
-
${renderViewTitle({
|
|
775
|
-
icon: html`<i class="far fa-list-alt mini-title"></i>`,
|
|
776
|
-
text: Translate.Render('results'),
|
|
777
|
-
})}
|
|
778
|
-
</div>`,
|
|
806
|
+
</div>`,
|
|
779
807
|
html: () => html``,
|
|
780
808
|
titleClass: 'mini-title',
|
|
781
809
|
style: {
|
|
@@ -787,6 +815,7 @@ const Modal = {
|
|
|
787
815
|
: '300px !important',
|
|
788
816
|
'z-index': 7,
|
|
789
817
|
},
|
|
818
|
+
class: 'search-history-modal',
|
|
790
819
|
dragDisabled: true,
|
|
791
820
|
maximize: true,
|
|
792
821
|
barMode: options.barMode,
|
|
@@ -804,6 +833,63 @@ const Modal = {
|
|
|
804
833
|
|
|
805
834
|
Modal.MoveTitleToBar(id);
|
|
806
835
|
|
|
836
|
+
// Add styles for inline button layout in the search history modal bar
|
|
837
|
+
const styleId = 'search-history-modal-bar-styles';
|
|
838
|
+
if (!s(`#${styleId}`)) {
|
|
839
|
+
const styleTag = document.createElement('style');
|
|
840
|
+
styleTag.id = styleId;
|
|
841
|
+
styleTag.textContent = `
|
|
842
|
+
.search-history-modal .btn-bar-modal-container .bar-default-modal {
|
|
843
|
+
display: flex;
|
|
844
|
+
flex-direction: row-reverse;
|
|
845
|
+
align-items: center;
|
|
846
|
+
}
|
|
847
|
+
.search-history-modal .btn-bar-modal-container .btn-modal-default {
|
|
848
|
+
display: inline-flex;
|
|
849
|
+
align-items: center;
|
|
850
|
+
justify-content: center;
|
|
851
|
+
float: none;
|
|
852
|
+
}
|
|
853
|
+
.search-history-modal .html-${searchBoxHistoryId} {
|
|
854
|
+
overflow-x: hidden;
|
|
855
|
+
box-sizing: border-box;
|
|
856
|
+
}
|
|
857
|
+
.search-history-modal .html-${searchBoxHistoryId} .search-result-item,
|
|
858
|
+
.search-history-modal .html-${searchBoxHistoryId} .search-result-history-item {
|
|
859
|
+
box-sizing: border-box;
|
|
860
|
+
max-width: 100%;
|
|
861
|
+
}
|
|
862
|
+
`;
|
|
863
|
+
document.head.appendChild(styleTag);
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
// Add clear all button to the bar area, before the close button
|
|
867
|
+
const clearAllBtnHtml = await BtnIcon.Render({
|
|
868
|
+
class: `btn-search-history-clear-all btn-modal-default btn-modal-default-${searchBoxHistoryId}`,
|
|
869
|
+
label: html`<i class="fas fa-trash-alt"></i>`,
|
|
870
|
+
attrs: `title="Clear all recent items"`,
|
|
871
|
+
style: 'padding: 4px 8px; font-size: 12px; display: none;',
|
|
872
|
+
});
|
|
873
|
+
|
|
874
|
+
// Insert before close button in the bar (with flex row-reverse, inserting after close button places our button to its left visually)
|
|
875
|
+
const closeBtn = s(`.btn-close-${searchBoxHistoryId}`);
|
|
876
|
+
if (closeBtn) {
|
|
877
|
+
closeBtn.insertAdjacentHTML('afterend', clearAllBtnHtml);
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
// Add click handler for clear all history button
|
|
881
|
+
const clearAllBtn = s(`.btn-search-history-clear-all`);
|
|
882
|
+
if (clearAllBtn) {
|
|
883
|
+
clearAllBtn.onclick = (e) => {
|
|
884
|
+
e.preventDefault();
|
|
885
|
+
e.stopPropagation();
|
|
886
|
+
// Clear all history from persistent storage
|
|
887
|
+
SearchBox.RecentResults.clear();
|
|
888
|
+
// Re-render to show empty history (with isRecentHistory true to hide button)
|
|
889
|
+
renderSearchResult([], true);
|
|
890
|
+
};
|
|
891
|
+
}
|
|
892
|
+
|
|
807
893
|
prepend(`.btn-bar-modal-container-${id}`, html`<div class="hide">${inputInfoNode.outerHTML}</div>`);
|
|
808
894
|
}
|
|
809
895
|
};
|
|
@@ -2020,6 +2106,34 @@ const Modal = {
|
|
|
2020
2106
|
},
|
|
2021
2107
|
mobileModal: () => windowGetW() < 600 || windowGetH() < 600,
|
|
2022
2108
|
writeHTML: ({ idModal, html }) => htmls(`.html-${idModal}`, html),
|
|
2109
|
+
updateModal: async function ({ idModal, html, title }) {
|
|
2110
|
+
if (!this.Data[idModal] || !s(`.${idModal}`)) {
|
|
2111
|
+
console.warn(`Modal ${idModal} not found for update`);
|
|
2112
|
+
return false;
|
|
2113
|
+
}
|
|
2114
|
+
|
|
2115
|
+
// Update modal content
|
|
2116
|
+
if (html) {
|
|
2117
|
+
this.writeHTML({ idModal, html });
|
|
2118
|
+
}
|
|
2119
|
+
|
|
2120
|
+
// Update modal title if provided
|
|
2121
|
+
if (title) {
|
|
2122
|
+
const titleElement = s(`.${idModal} .modal-title`);
|
|
2123
|
+
if (titleElement) {
|
|
2124
|
+
titleElement.innerHTML = title;
|
|
2125
|
+
}
|
|
2126
|
+
}
|
|
2127
|
+
|
|
2128
|
+
// Trigger reload listeners
|
|
2129
|
+
if (this.Data[idModal].onReloadModalListener) {
|
|
2130
|
+
for (const event of Object.keys(this.Data[idModal].onReloadModalListener)) {
|
|
2131
|
+
await this.Data[idModal].onReloadModalListener[event]();
|
|
2132
|
+
}
|
|
2133
|
+
}
|
|
2134
|
+
|
|
2135
|
+
return true;
|
|
2136
|
+
},
|
|
2023
2137
|
viewModalOpen: function () {
|
|
2024
2138
|
return Object.keys(this.Data).find((idModal) => s(`.${idModal}`) && this.Data[idModal].options.mode === 'view');
|
|
2025
2139
|
},
|