mbkbucket 1.2.1 → 1.3.0
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/index.js +1 -0
- package/lib/bucket.js +107 -52
- package/lib/s3.js +7 -2
- package/package.json +1 -1
- package/public/bucketadmin.css +164 -19
- package/public/bucketadmin.js +178 -61
- package/views/bucket.handlebars +1 -1
- package/views/bucketadmincontent.handlebars +14 -12
package/index.js
CHANGED
|
@@ -49,6 +49,7 @@ server.get('/mbkbucket/bucketadmin.css', (req, res) => {
|
|
|
49
49
|
if (err) res.status(err.status || 404).end();
|
|
50
50
|
});
|
|
51
51
|
});
|
|
52
|
+
|
|
52
53
|
server.get('/mbkbucket/bucketadmin.js', (req, res) => {
|
|
53
54
|
res.set('Cache-Control', `public, max-age=${staticMaxAgeSeconds}, immutable`);
|
|
54
55
|
res.sendFile(path.join(__dirname, 'public', 'bucketadmin.js'), (err) => {
|
package/lib/bucket.js
CHANGED
|
@@ -33,9 +33,7 @@ router.get(['/mbkbucket/info', "/mbkbucket/i"], validateSessionAndRole('SuperAdm
|
|
|
33
33
|
|
|
34
34
|
// API endpoint for fetching files
|
|
35
35
|
router.get('/mbkbucket/api/files', validateSessionAndRole('SuperAdmin'), async (req, res) => {
|
|
36
|
-
|
|
37
|
-
//return res.json({success: true,files: [],prefix: getAppName(),search: '',bucket: '',currentPage: 1,totalPages: 1,hasNextPage: false,hasPrevPage: false,nextPage: 2,prevPage: 1,totalFiles: 1});
|
|
38
|
-
const { prefix = '', page = '1', search = '' } = req.query;
|
|
36
|
+
const { prefix = '', page = '1', search = '', recursive = 'true', token = '' } = req.query;
|
|
39
37
|
const pageSize = 100;
|
|
40
38
|
let effectivePrefix;
|
|
41
39
|
try {
|
|
@@ -44,64 +42,121 @@ router.get('/mbkbucket/api/files', validateSessionAndRole('SuperAdmin'), async (
|
|
|
44
42
|
return res.status(400).json({ success: false, error: e.message });
|
|
45
43
|
}
|
|
46
44
|
|
|
45
|
+
// Determine mode: Optimized (Folder View, No Search) vs Legacy/Deep (Flat View or Search)
|
|
46
|
+
const isSearch = search && search.trim().length > 0;
|
|
47
|
+
// If recursive is explicitly 'false', we respect it. Default to true if missing (though frontend should send it)
|
|
48
|
+
// Logic: Optimization works only if NOT recursive and NOT searching.
|
|
49
|
+
const isRecursive = recursive === 'true';
|
|
50
|
+
const useOptimizedListing = !isSearch && !isRecursive;
|
|
51
|
+
|
|
47
52
|
try {
|
|
48
53
|
let files = [];
|
|
49
|
-
let
|
|
54
|
+
let folders = []; // For CommonPrefixes (subfolders)
|
|
55
|
+
let nextContinuationToken = null;
|
|
50
56
|
let usedPrefix = effectivePrefix;
|
|
51
57
|
let legacyFallback = false;
|
|
52
58
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
59
|
+
if (useOptimizedListing) {
|
|
60
|
+
// --- OPTIMIZED PATH: S3 Delimiter (Folder View) ---
|
|
61
|
+
// We process only one page from S3. This reduces API calls significantly.
|
|
62
|
+
|
|
63
|
+
// FIX: Ensure root listing uses trailing slash to prevent "app-name" matching "app-name-backup"
|
|
64
|
+
// and ensure directory browsing works correctly if ensurePrefix didn't append slash (which it now does if present in input).
|
|
65
|
+
// But for root, input is empty, ensurePrefix returns "app", so we force "app/" here.
|
|
66
|
+
let listPrefix = effectivePrefix;
|
|
67
|
+
if (listPrefix === getAppName() && !listPrefix.endsWith('/')) {
|
|
68
|
+
listPrefix += '/';
|
|
58
69
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
+
|
|
71
|
+
const result = await listfiles(listPrefix, {
|
|
72
|
+
continuationToken: token || undefined, // Use token from query if provided
|
|
73
|
+
delimiter: '/',
|
|
74
|
+
maxKeys: 1000 // Fetch a healthy batch.
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
files = result.Contents || [];
|
|
78
|
+
// S3 returns CommonPrefixes for folders when delimiter is used
|
|
79
|
+
folders = (result.CommonPrefixes || []).map(p => p.Prefix).filter(p => p !== effectivePrefix);
|
|
80
|
+
nextContinuationToken = result.NextContinuationToken;
|
|
81
|
+
|
|
82
|
+
// Note: In optimized mode, totalFiles is unknown without scanning everything.
|
|
83
|
+
} else {
|
|
84
|
+
// --- LEGACY PATH: Recursive Scan (Search or Flat View) ---
|
|
85
|
+
let continuationToken = undefined;
|
|
86
|
+
|
|
87
|
+
// Fetch all files recursively (handling S3 pagination)
|
|
88
|
+
do {
|
|
89
|
+
const result = await listfiles(effectivePrefix, { continuationToken });
|
|
90
|
+
if (result.Contents) {
|
|
91
|
+
files = files.concat(result.Contents);
|
|
70
92
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
93
|
+
continuationToken = result.nextToken;
|
|
94
|
+
} while (continuationToken);
|
|
95
|
+
|
|
96
|
+
// If no files found under the app prefixed path, try root as a legacy fallback
|
|
97
|
+
if (!files.length && !search) {
|
|
98
|
+
try {
|
|
99
|
+
const rootResult = await listfiles('');
|
|
100
|
+
if ((rootResult.Contents || []).length) {
|
|
101
|
+
files = rootResult.Contents || [];
|
|
102
|
+
usedPrefix = '';
|
|
103
|
+
legacyFallback = true;
|
|
104
|
+
}
|
|
105
|
+
} catch (fallbackErr) {
|
|
106
|
+
console.warn('Fallback root listing failed:', fallbackErr.message);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Apply search filter if provided
|
|
111
|
+
if (isSearch) {
|
|
112
|
+
const searchLower = search.toLowerCase();
|
|
113
|
+
files = files.filter(file => {
|
|
114
|
+
return file.Key.toLowerCase().includes(searchLower);
|
|
115
|
+
});
|
|
74
116
|
}
|
|
75
117
|
}
|
|
76
118
|
|
|
77
|
-
//
|
|
78
|
-
if (
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
119
|
+
// Response Construction
|
|
120
|
+
if (useOptimizedListing) {
|
|
121
|
+
res.json({
|
|
122
|
+
success: true,
|
|
123
|
+
files: files,
|
|
124
|
+
folders: folders,
|
|
125
|
+
prefix: effectivePrefix,
|
|
126
|
+
search: search,
|
|
127
|
+
// In optimized mode, we don't support random access "Page X". We support "Next".
|
|
128
|
+
currentPage: 1,
|
|
129
|
+
totalPages: nextContinuationToken ? 9999 : 1,
|
|
130
|
+
hasNextPage: !!nextContinuationToken,
|
|
131
|
+
nextContinuationToken: nextContinuationToken,
|
|
132
|
+
mode: 'optimized',
|
|
133
|
+
totalFiles: -1
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
} else {
|
|
137
|
+
// Legacy Pagination Logic (In-Memory)
|
|
138
|
+
const totalFiles = files.length;
|
|
139
|
+
const totalPages = Math.max(1, Math.ceil(totalFiles / pageSize));
|
|
140
|
+
const currentPage = Math.max(1, parseInt(page, 10) || 1);
|
|
141
|
+
const startIndex = (currentPage - 1) * pageSize;
|
|
142
|
+
const paginatedFiles = files.slice(startIndex, startIndex + pageSize);
|
|
143
|
+
|
|
144
|
+
res.json({
|
|
145
|
+
success: true,
|
|
146
|
+
files: paginatedFiles,
|
|
147
|
+
folders: [], // In recursive mode, frontend calculates folders from file paths
|
|
148
|
+
prefix: effectivePrefix,
|
|
149
|
+
search: search,
|
|
150
|
+
currentPage: currentPage,
|
|
151
|
+
totalPages,
|
|
152
|
+
hasNextPage: currentPage < totalPages,
|
|
153
|
+
hasPrevPage: currentPage > 1,
|
|
154
|
+
nextPage: currentPage + 1,
|
|
155
|
+
prevPage: Math.max(1, currentPage - 1),
|
|
156
|
+
totalFiles
|
|
157
|
+
});
|
|
83
158
|
}
|
|
84
159
|
|
|
85
|
-
// Pagination logic
|
|
86
|
-
const totalFiles = files.length;
|
|
87
|
-
const totalPages = Math.max(1, Math.ceil(totalFiles / pageSize));
|
|
88
|
-
const currentPage = Math.max(1, parseInt(page, 10) || 1);
|
|
89
|
-
const startIndex = (currentPage - 1) * pageSize;
|
|
90
|
-
const paginatedFiles = files.slice(startIndex, startIndex + pageSize);
|
|
91
|
-
|
|
92
|
-
res.json({
|
|
93
|
-
success: true,
|
|
94
|
-
files: paginatedFiles,
|
|
95
|
-
prefix: effectivePrefix,
|
|
96
|
-
search: search,
|
|
97
|
-
currentPage: currentPage,
|
|
98
|
-
totalPages,
|
|
99
|
-
hasNextPage: currentPage < totalPages,
|
|
100
|
-
hasPrevPage: currentPage > 1,
|
|
101
|
-
nextPage: currentPage + 1,
|
|
102
|
-
prevPage: Math.max(1, currentPage - 1),
|
|
103
|
-
totalFiles
|
|
104
|
-
});
|
|
105
160
|
} catch (error) {
|
|
106
161
|
console.error("Error listing files:", error);
|
|
107
162
|
res.status(500).json({
|
|
@@ -128,7 +183,7 @@ router.post('/mbkbucket/upload', validateSessionAndRole('SuperAdmin'), upload.si
|
|
|
128
183
|
return res.status(400).json({ success: false, error: e.message });
|
|
129
184
|
}
|
|
130
185
|
|
|
131
|
-
const key = `${effectivePrefix}/${req.file.originalname}`;
|
|
186
|
+
const key = `${effectivePrefix.replace(/\/+$/, '')}/${req.file.originalname}`;
|
|
132
187
|
|
|
133
188
|
// Check if file already exists
|
|
134
189
|
const exists = await fileExists(key);
|
|
@@ -196,7 +251,7 @@ router.post('/mbkbucket/upload-complete', validateSessionAndRole('SuperAdmin'),
|
|
|
196
251
|
// Upload to bucket
|
|
197
252
|
let effectivePrefix;
|
|
198
253
|
try { effectivePrefix = ensurePrefix(prefix); } catch (e) { return res.status(400).json({ success: false, error: e.message }); }
|
|
199
|
-
const key = `${effectivePrefix}/${fileName}`;
|
|
254
|
+
const key = `${effectivePrefix.replace(/\/+$/, '')}/${fileName}`;
|
|
200
255
|
|
|
201
256
|
// Check existence
|
|
202
257
|
const exists = await fileExists(key);
|
|
@@ -226,7 +281,7 @@ router.post('/mbkbucket/create-folder', validateSessionAndRole('SuperAdmin'), as
|
|
|
226
281
|
let effectivePrefix;
|
|
227
282
|
try { effectivePrefix = ensurePrefix(prefix); } catch (e) { return res.status(400).json({ success: false, error: e.message }); }
|
|
228
283
|
|
|
229
|
-
const folderKey = `${effectivePrefix}/${folderName}/`;
|
|
284
|
+
const folderKey = `${effectivePrefix.replace(/\/+$/, '')}/${folderName}/`;
|
|
230
285
|
|
|
231
286
|
// Check if exists
|
|
232
287
|
const exists = await fileExists(folderKey);
|
package/lib/s3.js
CHANGED
|
@@ -66,10 +66,15 @@ export function ensureKeyHasAppPrefix(key = '') {
|
|
|
66
66
|
export function ensurePrefix(prefix = '') {
|
|
67
67
|
const app = getAppName();
|
|
68
68
|
if (!app) throw new Error('APP_NAME is not configured; set mbkautheVar.APP_NAME or MBKAUTHE_APP_NAME');
|
|
69
|
-
|
|
69
|
+
// Only strip LEADING slashes. Preserve trailing slashes for directory listing.
|
|
70
|
+
let p = String(prefix || '').replace(/^\/+/, '');
|
|
71
|
+
|
|
70
72
|
if (!p) return `${app}`;
|
|
73
|
+
|
|
71
74
|
// If prefix already equals app or already starts with app/, normalize and return single app prefix
|
|
72
|
-
if (p === app) return app;
|
|
75
|
+
if (p === app) return app; // If p is exactly "app", we return "app". (Caller should append / if listing root)
|
|
76
|
+
if (p === `${app}/`) return `${app}/`;
|
|
77
|
+
|
|
73
78
|
if (p.startsWith(`${app}/`)) {
|
|
74
79
|
// collapse repeated app/ sequences
|
|
75
80
|
const appPrefixRegex = new RegExp(`^(?:${app}\/)+`);
|
package/package.json
CHANGED
package/public/bucketadmin.css
CHANGED
|
@@ -43,6 +43,48 @@
|
|
|
43
43
|
padding: 1rem;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
/* Collapsible Upload Section */
|
|
47
|
+
.upload-card-header {
|
|
48
|
+
display: flex;
|
|
49
|
+
justify-content: space-between;
|
|
50
|
+
align-items: center;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.collapse-btn {
|
|
54
|
+
background: none;
|
|
55
|
+
border: none;
|
|
56
|
+
color: white;
|
|
57
|
+
padding: 0.5rem;
|
|
58
|
+
border-radius: 4px;
|
|
59
|
+
transition: all 0.3s ease;
|
|
60
|
+
cursor: pointer;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.collapse-btn:hover {
|
|
64
|
+
background: rgba(255, 255, 255, 0.1);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.collapse-btn i {
|
|
68
|
+
transition: transform 0.3s ease;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.collapse-btn.expanded i {
|
|
72
|
+
transform: rotate(180deg);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.collapsible-content {
|
|
76
|
+
padding: 0rem !important;
|
|
77
|
+
max-height: 0;
|
|
78
|
+
overflow: hidden;
|
|
79
|
+
transition: max-height 0.3s ease-out;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.collapsible-content.expanded {
|
|
83
|
+
padding: 1rem !important;
|
|
84
|
+
max-height: 2000px;
|
|
85
|
+
transition: max-height 0.3s ease-in;
|
|
86
|
+
}
|
|
87
|
+
|
|
46
88
|
/* File Stats Card - Welcome Style */
|
|
47
89
|
.file-stats {
|
|
48
90
|
background: linear-gradient(135deg, var(--primary-color-ml) 0%, var(--primary-dark-color-ml) 100%);
|
|
@@ -1077,32 +1119,30 @@
|
|
|
1077
1119
|
align-items: center;
|
|
1078
1120
|
}
|
|
1079
1121
|
|
|
1080
|
-
/* Skeleton loaders */
|
|
1122
|
+
/* Skeleton loaders - tuned grid to equalize column spacing visually */
|
|
1081
1123
|
.skeleton-row {
|
|
1082
|
-
display:
|
|
1124
|
+
display: grid;
|
|
1125
|
+
grid-template-columns: 48px minmax(120px, 1fr) minmax(120px, 1fr) minmax(120px, 240px); /* icon + 3 columns with balanced min widths */
|
|
1083
1126
|
gap: 1rem;
|
|
1084
1127
|
padding: 1rem;
|
|
1128
|
+
align-items: center;
|
|
1129
|
+
justify-items: stretch; /* make cells span their tracks evenly */
|
|
1085
1130
|
}
|
|
1086
1131
|
|
|
1132
|
+
/* make skeleton boxes flexible and responsive */
|
|
1087
1133
|
.skeleton-box {
|
|
1088
1134
|
background: linear-gradient(90deg, #f5f7fa, #eef2f6);
|
|
1089
1135
|
border-radius: 6px;
|
|
1090
|
-
}
|
|
1091
|
-
|
|
1092
|
-
.skeleton-box.s-80 {
|
|
1093
|
-
width: 80px;
|
|
1094
1136
|
height: 16px;
|
|
1137
|
+
width: 100%;
|
|
1138
|
+
min-width: 0;
|
|
1139
|
+
justify-self: stretch;
|
|
1095
1140
|
}
|
|
1096
1141
|
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
}
|
|
1101
|
-
|
|
1102
|
-
.skeleton-box.s-40 {
|
|
1103
|
-
width: 40px;
|
|
1104
|
-
height: 16px;
|
|
1105
|
-
}
|
|
1142
|
+
/* semantic size hints (max widths) and alignment to ensure visual balance */
|
|
1143
|
+
.skeleton-box.s-40 { max-width: 48px; justify-self: start; }
|
|
1144
|
+
.skeleton-box.s-80 { max-width: 160px; justify-self: center; }
|
|
1145
|
+
.skeleton-box.s-200 { max-width: 220px; justify-self: end; }
|
|
1106
1146
|
|
|
1107
1147
|
.skeleton-row .file-icon {
|
|
1108
1148
|
width: 40px;
|
|
@@ -1110,6 +1150,26 @@
|
|
|
1110
1150
|
border-radius: 8px;
|
|
1111
1151
|
}
|
|
1112
1152
|
|
|
1153
|
+
/* Responsive adjustments */
|
|
1154
|
+
@media (max-width: 992px) {
|
|
1155
|
+
.skeleton-row {
|
|
1156
|
+
grid-template-columns: 40px minmax(100px, 1fr) minmax(100px, 1fr) minmax(100px, 200px);
|
|
1157
|
+
gap: 0.9rem;
|
|
1158
|
+
}
|
|
1159
|
+
.skeleton-box.s-200 { max-width: 180px; }
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
@media (max-width: 768px) {
|
|
1163
|
+
.skeleton-row {
|
|
1164
|
+
grid-template-columns: 40px 1fr 1fr 1fr;
|
|
1165
|
+
gap: 0.75rem;
|
|
1166
|
+
}
|
|
1167
|
+
.skeleton-box.s-80 { justify-self: start; }
|
|
1168
|
+
.skeleton-box.s-200 { max-width: 140px; justify-self: end; }
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
|
|
1172
|
+
|
|
1113
1173
|
.btn:disabled {
|
|
1114
1174
|
opacity: 0.5;
|
|
1115
1175
|
cursor: not-allowed;
|
|
@@ -1187,12 +1247,21 @@
|
|
|
1187
1247
|
}
|
|
1188
1248
|
|
|
1189
1249
|
.stats-content {
|
|
1190
|
-
|
|
1191
|
-
text-align: center;
|
|
1250
|
+
gap: 0.75rem;
|
|
1192
1251
|
}
|
|
1193
1252
|
|
|
1194
1253
|
.stats-icon {
|
|
1195
|
-
|
|
1254
|
+
width: 50px;
|
|
1255
|
+
height: 50px;
|
|
1256
|
+
font-size: 1.3rem;
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
.stats-text h2 {
|
|
1260
|
+
font-size: 1.3rem;
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
.stats-text p {
|
|
1264
|
+
font-size: 0.85rem;
|
|
1196
1265
|
}
|
|
1197
1266
|
|
|
1198
1267
|
.filter-form {
|
|
@@ -1209,8 +1278,23 @@
|
|
|
1209
1278
|
padding: 1rem;
|
|
1210
1279
|
}
|
|
1211
1280
|
|
|
1281
|
+
.stats-content {
|
|
1282
|
+
gap: 0.5rem;
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
.stats-icon {
|
|
1286
|
+
width: 40px;
|
|
1287
|
+
height: 40px;
|
|
1288
|
+
font-size: 1.2rem;
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1212
1291
|
.stats-text h2 {
|
|
1213
|
-
font-size: 1.
|
|
1292
|
+
font-size: 1.2rem;
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
.stats-text p {
|
|
1296
|
+
font-size: 0.8rem;
|
|
1297
|
+
margin: 0.25rem 0 0;
|
|
1214
1298
|
}
|
|
1215
1299
|
|
|
1216
1300
|
.btn-group {
|
|
@@ -1481,6 +1565,67 @@
|
|
|
1481
1565
|
display: none;
|
|
1482
1566
|
}
|
|
1483
1567
|
|
|
1568
|
+
/* Additional utility classes for removing inline styles */
|
|
1569
|
+
.file-list-header {
|
|
1570
|
+
display: flex;
|
|
1571
|
+
justify-content: space-between;
|
|
1572
|
+
align-items: center;
|
|
1573
|
+
flex-wrap: wrap;
|
|
1574
|
+
gap: 1rem;
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
.search-inline {
|
|
1578
|
+
flex: 1;
|
|
1579
|
+
max-width: 640px;
|
|
1580
|
+
display: flex;
|
|
1581
|
+
gap: 0.5rem;
|
|
1582
|
+
align-items: center;
|
|
1583
|
+
justify-content: center;
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
.view-btn-left {
|
|
1587
|
+
border-top-right-radius: 0;
|
|
1588
|
+
border-bottom-right-radius: 0;
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
.view-btn-right {
|
|
1592
|
+
border-top-left-radius: 0;
|
|
1593
|
+
border-bottom-left-radius: 0;
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
.pagination-container {
|
|
1597
|
+
display: none;
|
|
1598
|
+
padding: 1rem;
|
|
1599
|
+
border-top: 1px solid var(--gray-light-color-ml);
|
|
1600
|
+
background: var(--light-color-ml);
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
.modal-body-text-style {
|
|
1604
|
+
color: var(--gray-color-ml);
|
|
1605
|
+
margin-bottom: 0;
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
.modal-body-text-top {
|
|
1609
|
+
color: var(--gray-color-ml);
|
|
1610
|
+
margin-top: 0.5rem;
|
|
1611
|
+
margin-bottom: 0;
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1614
|
+
.modal-header-spacing {
|
|
1615
|
+
margin-top: 0.5rem;
|
|
1616
|
+
color: var(--gray-color-ml);
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
.toast-container {
|
|
1620
|
+
position: fixed;
|
|
1621
|
+
bottom: 20px;
|
|
1622
|
+
right: 20px;
|
|
1623
|
+
z-index: 1050;
|
|
1624
|
+
display: flex;
|
|
1625
|
+
flex-direction: column;
|
|
1626
|
+
gap: 10px;
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1484
1629
|
@media (max-width: 768px) {
|
|
1485
1630
|
.dtm {
|
|
1486
1631
|
display: block;
|
package/public/bucketadmin.js
CHANGED
|
@@ -7,6 +7,7 @@ let currentOrder = 'asc';
|
|
|
7
7
|
let currentViewMode = 'folder'; // 'folder' or 'flat'
|
|
8
8
|
let pageFiles = [];
|
|
9
9
|
let pageFolders = [];
|
|
10
|
+
let currentNextToken = null;
|
|
10
11
|
const bucketAppName = document.getElementById('bucketAppData')?.dataset.appName || '';
|
|
11
12
|
// Bucket is configured centrally on the server; client should not select a bucket.
|
|
12
13
|
|
|
@@ -70,7 +71,7 @@ function showAlert(message, type = 'success') {
|
|
|
70
71
|
}
|
|
71
72
|
|
|
72
73
|
// Fetch and render files
|
|
73
|
-
async function loadFiles(page = 1, prefix = '', search = '', sort = currentSort, order = currentOrder) {
|
|
74
|
+
async function loadFiles(page = 1, prefix = '', search = '', sort = currentSort, order = currentOrder, token = '') {
|
|
74
75
|
currentPage = page;
|
|
75
76
|
currentPrefix = prefix;
|
|
76
77
|
currentSearch = search;
|
|
@@ -109,19 +110,13 @@ async function loadFiles(page = 1, prefix = '', search = '', sort = currentSort,
|
|
|
109
110
|
<div class="skeleton-box s-40"></div>
|
|
110
111
|
<div class="skeleton-box s-200"></div>
|
|
111
112
|
<div class="skeleton-box s-80"></div>
|
|
112
|
-
<div class="skeleton-box s-80"></div>
|
|
113
|
-
</div>
|
|
114
|
-
<div class="skeleton-row">
|
|
115
|
-
<div class="skeleton-box s-40"></div>
|
|
116
113
|
<div class="skeleton-box s-200"></div>
|
|
117
|
-
<div class="skeleton-box s-80"></div>
|
|
118
|
-
<div class="skeleton-box s-80"></div>
|
|
119
114
|
</div>
|
|
120
115
|
<div class="skeleton-row">
|
|
121
116
|
<div class="skeleton-box s-40"></div>
|
|
122
117
|
<div class="skeleton-box s-200"></div>
|
|
123
118
|
<div class="skeleton-box s-80"></div>
|
|
124
|
-
<div class="skeleton-box s-
|
|
119
|
+
<div class="skeleton-box s-200"></div>
|
|
125
120
|
</div>
|
|
126
121
|
</div>
|
|
127
122
|
`;
|
|
@@ -129,15 +124,28 @@ async function loadFiles(page = 1, prefix = '', search = '', sort = currentSort,
|
|
|
129
124
|
try {
|
|
130
125
|
const searchParam = search ? `&search=${encodeURIComponent(search)}` : '';
|
|
131
126
|
const sortParam = `&sort=${sort}&order=${order}`;
|
|
132
|
-
|
|
127
|
+
|
|
128
|
+
// Determine recursive mode
|
|
129
|
+
const isFolderMode = currentViewMode === 'folder';
|
|
130
|
+
const recursiveParam = `&recursive=${!isFolderMode}`; // false if folder mode, true if flat mode
|
|
131
|
+
const tokenParam = token ? `&token=${encodeURIComponent(token)}` : '';
|
|
132
|
+
|
|
133
|
+
const response = await fetch(`/mbkbucket/api/files?page=${page}&prefix=${encodeURIComponent(prefix)}${searchParam}${sortParam}${recursiveParam}${tokenParam}`);
|
|
133
134
|
const data = await response.json();
|
|
134
135
|
|
|
135
136
|
if (!data.success) {
|
|
136
137
|
throw new Error(data.error || 'Failed to load files');
|
|
137
138
|
}
|
|
139
|
+
|
|
140
|
+
// Store next token
|
|
141
|
+
currentNextToken = data.nextContinuationToken || null;
|
|
138
142
|
|
|
139
143
|
// Update stats
|
|
140
|
-
|
|
144
|
+
if (data.totalFiles === -1) {
|
|
145
|
+
totalFilesCount.textContent = `${(data.files.length + (data.folders ? data.folders.length : 0))}${data.hasNextPage ? '+' : ''} Items`;
|
|
146
|
+
} else {
|
|
147
|
+
totalFilesCount.textContent = `${data.totalFiles} ${search ? 'Results' : 'Files'}`;
|
|
148
|
+
}
|
|
141
149
|
|
|
142
150
|
if (search) {
|
|
143
151
|
filterInfo.textContent = `Search results for: "${search}"`;
|
|
@@ -149,7 +157,9 @@ async function loadFiles(page = 1, prefix = '', search = '', sort = currentSort,
|
|
|
149
157
|
}
|
|
150
158
|
|
|
151
159
|
// Render files or empty state
|
|
152
|
-
|
|
160
|
+
const hasContent = (data.files && data.files.length > 0) || (data.folders && data.folders.length > 0);
|
|
161
|
+
|
|
162
|
+
if (!hasContent && !prefix && !search) {
|
|
153
163
|
container.innerHTML = `
|
|
154
164
|
<div class="empty-state">
|
|
155
165
|
<i class="fas fa-folder-open"></i>
|
|
@@ -163,11 +173,13 @@ async function loadFiles(page = 1, prefix = '', search = '', sort = currentSort,
|
|
|
163
173
|
paginationContainer.style.display = 'none';
|
|
164
174
|
} else {
|
|
165
175
|
// Store for client-side sorting and view switching
|
|
166
|
-
pageFiles = data.files;
|
|
176
|
+
pageFiles = data.files || [];
|
|
167
177
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
178
|
+
if (data.mode === 'optimized') {
|
|
179
|
+
pageFolders = data.folders || [];
|
|
180
|
+
} else {
|
|
181
|
+
pageFolders = search ? [] : extractFoldersAtCurrentLevel(data.files, prefix);
|
|
182
|
+
}
|
|
171
183
|
|
|
172
184
|
renderFilesTable(pageFiles, pageFolders, prefix);
|
|
173
185
|
renderPagination(data);
|
|
@@ -400,7 +412,10 @@ function renderFilesTable(files, folders = [], prefix = '') {
|
|
|
400
412
|
|
|
401
413
|
// Generate folder rows (only used in folder mode)
|
|
402
414
|
const folderRows = displayFolders.map(folderPath => {
|
|
403
|
-
|
|
415
|
+
// Handle folder paths which typically end in a slash (e.g., "folder/subfolder/")
|
|
416
|
+
const cleanPath = folderPath.replace(/\/+$/, '');
|
|
417
|
+
const folderName = cleanPath.split('/').pop();
|
|
418
|
+
|
|
404
419
|
return `
|
|
405
420
|
<tr class="folder-row" onclick="loadFiles(1, '${folderPath}')" title="Open folder: ${folderName}">
|
|
406
421
|
<td style="text-align:center;">
|
|
@@ -453,10 +468,10 @@ function renderFilesTable(files, folders = [], prefix = '') {
|
|
|
453
468
|
folderPath = fullPath.substring(0, lastSlashIndex + 1); // include trailing slash
|
|
454
469
|
}
|
|
455
470
|
|
|
456
|
-
const iconClass = getFileIcon(filename);
|
|
457
|
-
const typeCategory = getFileTypeCategory(filename);
|
|
458
|
-
const truncatedName = truncateFileName(filename);
|
|
459
|
-
const isFileViewable = isViewable(filename);
|
|
471
|
+
const iconClass = window.getFileIcon ? window.getFileIcon(filename) : 'fas fa-file';
|
|
472
|
+
const typeCategory = window.getFileTypeCategory ? window.getFileTypeCategory(filename) : '';
|
|
473
|
+
const truncatedName = (window.truncateFileName ? window.truncateFileName(filename) : filename) || filename;
|
|
474
|
+
const isFileViewable = window.isViewable ? window.isViewable(filename) : false;
|
|
460
475
|
|
|
461
476
|
// If in flat mode, we might want to show the folder path more prominently
|
|
462
477
|
const pathDisplay = (currentViewMode === 'flat' && folderPath && folderPath !== prefix + (prefix ? '/' : ''))
|
|
@@ -493,6 +508,9 @@ function renderFilesTable(files, folders = [], prefix = '') {
|
|
|
493
508
|
data-key-enc="${encodedKey}" title="${isFileViewable ? 'View file in browser' : 'File type not supported for viewing'}" ${!isFileViewable ? 'disabled style="opacity:0.5"' : ''}>
|
|
494
509
|
<i class="fas fa-eye"></i>
|
|
495
510
|
</button>
|
|
511
|
+
<button type="button" class="btn btn-info btn-sm copy-btn" data-key-enc="${encodedKey}" title="Copy Link" style="color:white;">
|
|
512
|
+
<i class="fas fa-link"></i>
|
|
513
|
+
</button>
|
|
496
514
|
<a href="/mbkbucket/download/${encodedKey}" class="btn btn-success btn-sm"
|
|
497
515
|
title="Download file">
|
|
498
516
|
<i class="fas fa-download"></i>
|
|
@@ -587,63 +605,137 @@ function renderFilesTable(files, folders = [], prefix = '') {
|
|
|
587
605
|
|
|
588
606
|
// Render pagination
|
|
589
607
|
function renderPagination(data) {
|
|
590
|
-
const { currentPage, totalPages, hasPrevPage, hasNextPage } = data;
|
|
608
|
+
const { currentPage, totalPages, hasPrevPage, hasNextPage, mode } = data;
|
|
591
609
|
|
|
592
610
|
let paginationHTML = '';
|
|
593
611
|
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
<a class="page-link" href="#" onclick="event.preventDefault(); loadFiles(${currentPage - 1}, '${currentPrefix}', '${currentSearch}')">
|
|
599
|
-
<i class="fas fa-chevron-left"></i> Previous
|
|
600
|
-
</a>
|
|
601
|
-
</li>
|
|
602
|
-
`;
|
|
603
|
-
} else {
|
|
612
|
+
if (mode === 'optimized') {
|
|
613
|
+
// Optimized Mode: Token-based pagination (Next only, limited Prev)
|
|
614
|
+
|
|
615
|
+
// Previous button (Disabled for now as we don't track history stack of tokens)
|
|
604
616
|
paginationHTML += `
|
|
605
617
|
<li class="page-item disabled">
|
|
606
|
-
<span class="page-link">
|
|
618
|
+
<span class="page-link" title="Previous page not supported in optimized view">
|
|
607
619
|
<i class="fas fa-chevron-left"></i> Previous
|
|
608
620
|
</span>
|
|
609
621
|
</li>
|
|
610
622
|
`;
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
for (let i = 1; i <= totalPages; i++) {
|
|
615
|
-
if (i === currentPage) {
|
|
616
|
-
paginationHTML += `
|
|
623
|
+
|
|
624
|
+
// Current Page Indicator
|
|
625
|
+
paginationHTML += `
|
|
617
626
|
<li class="page-item active">
|
|
618
|
-
<span class="page-link">${
|
|
627
|
+
<span class="page-link">${currentPage}</span>
|
|
619
628
|
</li>
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
629
|
+
`;
|
|
630
|
+
|
|
631
|
+
// Next button
|
|
632
|
+
if (hasNextPage) {
|
|
633
|
+
// Quote the token correctly
|
|
634
|
+
const tokenStr = currentNextToken ? currentNextToken.replace(/'/g, "\\'") : "";
|
|
635
|
+
paginationHTML += `
|
|
623
636
|
<li class="page-item">
|
|
624
|
-
<a class="page-link" href="#" onclick="event.preventDefault(); loadFiles(${
|
|
637
|
+
<a class="page-link" href="#" onclick="event.preventDefault(); loadFiles(${currentPage + 1}, '${currentPrefix}', '${currentSearch}', '${currentSort}', '${currentOrder}', '${tokenStr}')">
|
|
638
|
+
Next <i class="fas fa-chevron-right"></i>
|
|
639
|
+
</a>
|
|
625
640
|
</li>
|
|
626
641
|
`;
|
|
627
|
-
|
|
628
|
-
|
|
642
|
+
} else {
|
|
643
|
+
paginationHTML += `
|
|
644
|
+
<li class="page-item disabled">
|
|
645
|
+
<span class="page-link">
|
|
646
|
+
Next <i class="fas fa-chevron-right"></i>
|
|
647
|
+
</span>
|
|
648
|
+
</li>
|
|
649
|
+
`;
|
|
650
|
+
}
|
|
629
651
|
|
|
630
|
-
// Next button
|
|
631
|
-
if (hasNextPage) {
|
|
632
|
-
paginationHTML += `
|
|
633
|
-
<li class="page-item">
|
|
634
|
-
<a class="page-link" href="#" onclick="event.preventDefault(); loadFiles(${currentPage + 1}, '${currentPrefix}', '${currentSearch}')">
|
|
635
|
-
Next <i class="fas fa-chevron-right"></i>
|
|
636
|
-
</a>
|
|
637
|
-
</li>
|
|
638
|
-
`;
|
|
639
652
|
} else {
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
653
|
+
// Legacy Mode: Numbered pagination
|
|
654
|
+
|
|
655
|
+
// Previous button
|
|
656
|
+
if (hasPrevPage) {
|
|
657
|
+
paginationHTML += `
|
|
658
|
+
<li class="page-item">
|
|
659
|
+
<a class="page-link" href="#" onclick="event.preventDefault(); loadFiles(${currentPage - 1}, '${currentPrefix}', '${currentSearch}')">
|
|
660
|
+
<i class="fas fa-chevron-left"></i> Previous
|
|
661
|
+
</a>
|
|
662
|
+
</li>
|
|
663
|
+
`;
|
|
664
|
+
} else {
|
|
665
|
+
paginationHTML += `
|
|
666
|
+
<li class="page-item disabled">
|
|
667
|
+
<span class="page-link">
|
|
668
|
+
<i class="fas fa-chevron-left"></i> Previous
|
|
669
|
+
</span>
|
|
670
|
+
</li>
|
|
671
|
+
`;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
// Smart Page Numbers (limit visible pages)
|
|
675
|
+
const maxPagesToShow = 7;
|
|
676
|
+
let startPage = Math.max(1, currentPage - Math.floor(maxPagesToShow / 2));
|
|
677
|
+
let endPage = Math.min(totalPages, startPage + maxPagesToShow - 1);
|
|
678
|
+
|
|
679
|
+
if (endPage - startPage + 1 < maxPagesToShow) {
|
|
680
|
+
startPage = Math.max(1, endPage - maxPagesToShow + 1);
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
// First page
|
|
684
|
+
if (startPage > 1) {
|
|
685
|
+
paginationHTML += `
|
|
686
|
+
<li class="page-item">
|
|
687
|
+
<a class="page-link" href="#" onclick="event.preventDefault(); loadFiles(1, '${currentPrefix}', '${currentSearch}')">1</a>
|
|
688
|
+
</li>`;
|
|
689
|
+
if (startPage > 2) {
|
|
690
|
+
paginationHTML += `<li class="page-item disabled"><span class="page-link">...</span></li>`;
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
for (let i = startPage; i <= endPage; i++) {
|
|
695
|
+
if (i === currentPage) {
|
|
696
|
+
paginationHTML += `
|
|
697
|
+
<li class="page-item active">
|
|
698
|
+
<span class="page-link">${i}</span>
|
|
699
|
+
</li>
|
|
700
|
+
`;
|
|
701
|
+
} else {
|
|
702
|
+
paginationHTML += `
|
|
703
|
+
<li class="page-item">
|
|
704
|
+
<a class="page-link" href="#" onclick="event.preventDefault(); loadFiles(${i}, '${currentPrefix}', '${currentSearch}')">${i}</a>
|
|
705
|
+
</li>
|
|
706
|
+
`;
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
// Last page
|
|
711
|
+
if (endPage < totalPages) {
|
|
712
|
+
if (endPage < totalPages - 1) {
|
|
713
|
+
paginationHTML += `<li class="page-item disabled"><span class="page-link">...</span></li>`;
|
|
714
|
+
}
|
|
715
|
+
paginationHTML += `
|
|
716
|
+
<li class="page-item">
|
|
717
|
+
<a class="page-link" href="#" onclick="event.preventDefault(); loadFiles(${totalPages}, '${currentPrefix}', '${currentSearch}')">${totalPages}</a>
|
|
718
|
+
</li>`;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
// Next button
|
|
722
|
+
if (hasNextPage) {
|
|
723
|
+
paginationHTML += `
|
|
724
|
+
<li class="page-item">
|
|
725
|
+
<a class="page-link" href="#" onclick="event.preventDefault(); loadFiles(${currentPage + 1}, '${currentPrefix}', '${currentSearch}')">
|
|
726
|
+
Next <i class="fas fa-chevron-right"></i>
|
|
727
|
+
</a>
|
|
728
|
+
</li>
|
|
729
|
+
`;
|
|
730
|
+
} else {
|
|
731
|
+
paginationHTML += `
|
|
732
|
+
<li class="page-item disabled">
|
|
733
|
+
<span class="page-link">
|
|
734
|
+
Next <i class="fas fa-chevron-right"></i>
|
|
735
|
+
</span>
|
|
736
|
+
</li>
|
|
737
|
+
`;
|
|
738
|
+
}
|
|
647
739
|
}
|
|
648
740
|
|
|
649
741
|
document.getElementById('paginationList').innerHTML = paginationHTML;
|
|
@@ -915,8 +1007,33 @@ document.getElementById('clearQueueBtn')?.addEventListener('click', function ()
|
|
|
915
1007
|
|
|
916
1008
|
|
|
917
1009
|
|
|
1010
|
+
// Initialize upload section collapse functionality
|
|
1011
|
+
function initializeUploadCollapse() {
|
|
1012
|
+
const toggleBtn = document.getElementById('uploadToggleBtn');
|
|
1013
|
+
const uploadSection = document.getElementById('uploadSection');
|
|
1014
|
+
|
|
1015
|
+
if (!toggleBtn || !uploadSection) return;
|
|
1016
|
+
|
|
1017
|
+
toggleBtn.addEventListener('click', function() {
|
|
1018
|
+
const isExpanded = uploadSection.classList.contains('expanded');
|
|
1019
|
+
|
|
1020
|
+
if (isExpanded) {
|
|
1021
|
+
// Collapse
|
|
1022
|
+
uploadSection.classList.remove('expanded');
|
|
1023
|
+
toggleBtn.classList.remove('expanded');
|
|
1024
|
+
} else {
|
|
1025
|
+
// Expand
|
|
1026
|
+
uploadSection.classList.add('expanded');
|
|
1027
|
+
toggleBtn.classList.add('expanded');
|
|
1028
|
+
}
|
|
1029
|
+
});
|
|
1030
|
+
}
|
|
1031
|
+
|
|
918
1032
|
// Add hover effects and animations
|
|
919
1033
|
document.addEventListener('DOMContentLoaded', function () {
|
|
1034
|
+
// Initialize collapsible upload section
|
|
1035
|
+
initializeUploadCollapse();
|
|
1036
|
+
|
|
920
1037
|
// Load initial files (support both legacy 'prefix' and new 'folder' query param)
|
|
921
1038
|
const urlParams = new URLSearchParams(window.location.search);
|
|
922
1039
|
const initialPrefix = urlParams.get('folder') || urlParams.get('prefix') || bucketAppName || '';
|
package/views/bucket.handlebars
CHANGED
|
@@ -40,15 +40,18 @@
|
|
|
40
40
|
<!-- Upload Form -->
|
|
41
41
|
<div class="col-lg-12">
|
|
42
42
|
<div class="dashboard-card h-100">
|
|
43
|
-
<div class="card-header">
|
|
43
|
+
<div class="card-header upload-card-header" id="uploadToggleBtn">
|
|
44
44
|
<h5 class="card-title">
|
|
45
45
|
<div class="card-icon">
|
|
46
46
|
<i class="fas fa-upload"></i>
|
|
47
47
|
</div>
|
|
48
48
|
Upload File
|
|
49
49
|
</h5>
|
|
50
|
+
<button type="button" class="collapse-btn">
|
|
51
|
+
<i class="fas fa-chevron-down"></i>
|
|
52
|
+
</button>
|
|
50
53
|
</div>
|
|
51
|
-
<div class="card-body">
|
|
54
|
+
<div class="card-body collapsible-content" id="uploadSection">
|
|
52
55
|
<form id="uploadForm" action="/mbkbucket/upload" method="post" enctype="multipart/form-data">
|
|
53
56
|
<input type="hidden" name="prefix" value="{{prefix}}">
|
|
54
57
|
|
|
@@ -103,7 +106,7 @@
|
|
|
103
106
|
|
|
104
107
|
<!-- File List -->
|
|
105
108
|
<div class="dashboard-card">
|
|
106
|
-
<div class="card-header
|
|
109
|
+
<div class="card-header file-list-header">
|
|
107
110
|
<h5 class="card-title">
|
|
108
111
|
<div>
|
|
109
112
|
<i class="fas fa-folder-open"></i>
|
|
@@ -112,7 +115,7 @@
|
|
|
112
115
|
</h5>
|
|
113
116
|
|
|
114
117
|
<!-- Inline Search (moved from separate card) -->
|
|
115
|
-
<div class="search-inline"
|
|
118
|
+
<div class="search-inline">
|
|
116
119
|
<input type="text" class="form-control form-control-sm" id="searchInput" placeholder="Search files and folders..." aria-label="Search files">
|
|
117
120
|
<button type="button" class="btn btn-sm btn-primary" onclick="performSearch()" aria-label="Search"><i class="fas fa-search"></i></button>
|
|
118
121
|
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="clearSearch()" aria-label="Clear search"><i class="fas fa-times"></i></button>
|
|
@@ -128,10 +131,10 @@
|
|
|
128
131
|
</div>
|
|
129
132
|
|
|
130
133
|
<div class="btn-group" role="group" aria-label="View Mode">
|
|
131
|
-
<button type="button" class="btn btn-sm btn-light view-toggle-btn active" id="viewBtnFolder" onclick="changeViewMode('folder')" title="Folder View"
|
|
134
|
+
<button type="button" class="btn btn-sm btn-light view-toggle-btn view-btn-left active" id="viewBtnFolder" onclick="changeViewMode('folder')" title="Folder View">
|
|
132
135
|
<i class="fas fa-folder"></i> Folders
|
|
133
136
|
</button>
|
|
134
|
-
<button type="button" class="btn btn-sm btn-outline-light view-toggle-btn" id="viewBtnFlat" onclick="changeViewMode('flat')" title="Flat List (All Files)"
|
|
137
|
+
<button type="button" class="btn btn-sm btn-outline-light view-toggle-btn view-btn-right" id="viewBtnFlat" onclick="changeViewMode('flat')" title="Flat List (All Files)">
|
|
135
138
|
<i class="fas fa-list-ul"></i> Flat View
|
|
136
139
|
</button>
|
|
137
140
|
</div>
|
|
@@ -146,8 +149,7 @@
|
|
|
146
149
|
</div>
|
|
147
150
|
|
|
148
151
|
<!-- Pagination -->
|
|
149
|
-
<div id="paginationContainer"
|
|
150
|
-
style="display: none; padding: 1rem; border-top: 1px solid var(--gray-light-color-ml); background: var(--light-color-ml);">
|
|
152
|
+
<div id="paginationContainer" class="pagination-container">
|
|
151
153
|
<nav aria-label="File pagination">
|
|
152
154
|
<ul class="pagination" id="paginationList">
|
|
153
155
|
</ul>
|
|
@@ -171,10 +173,10 @@
|
|
|
171
173
|
<div class="modal-body">
|
|
172
174
|
<i class="fas fa-trash-alt fa-3x"></i>
|
|
173
175
|
<h6>Are you sure you want to delete this file?</h6>
|
|
174
|
-
<p
|
|
176
|
+
<p class="modal-body-text-style">
|
|
175
177
|
<strong id="deleteFileName" style="color: var(--dark-color-ml);"></strong>
|
|
176
178
|
</p>
|
|
177
|
-
<p
|
|
179
|
+
<p class="modal-body-text-top">
|
|
178
180
|
<small>This action cannot be undone.</small>
|
|
179
181
|
</p>
|
|
180
182
|
</div>
|
|
@@ -214,7 +216,7 @@
|
|
|
214
216
|
<div class="preview-content" id="previewContent" aria-live="polite">
|
|
215
217
|
<div class="empty-preview">
|
|
216
218
|
<i class="fas fa-spinner fa-spin"></i>
|
|
217
|
-
<div
|
|
219
|
+
<div class="modal-header-spacing">Loading preview...</div>
|
|
218
220
|
</div>
|
|
219
221
|
</div>
|
|
220
222
|
</div>
|
|
@@ -232,6 +234,6 @@
|
|
|
232
234
|
</div>
|
|
233
235
|
|
|
234
236
|
<!-- Toast Container -->
|
|
235
|
-
<div id="toast-container"
|
|
237
|
+
<div id="toast-container" class="toast-container"></div>
|
|
236
238
|
|
|
237
239
|
<script src="/mbkbucket/bucketadmin.js?v={{assetVersion}}"></script>
|