@underpostnet/underpost
Version:
Underpost Platform — end-to-end CI/CD and application-delivery toolchain CLI. Covers bare metal, Kubernetes, K3s, kubeadm, LXD, container/image orchestration, secrets, databases, cron jobs, monitoring, SSH, runners, PWA + Workbox delivery, and release orc
1,113 lines (1,108 loc) • 61.9 kB
JavaScript
import { getApiBaseUrl } from '../../services/core/core.service.js';
import { DocumentService } from '../../services/document/document.service.js';
import { FileService } from '../../services/file/file.service.js';
import { AgGrid } from './AgGrid.js';
import { Auth } from './Auth.js';
import { BtnIcon } from './BtnIcon.js';
import { getSubpaths, uniqueArray } from './CommonJs.js';
import { Css, darkTheme, dynamicCol, renderCssAttr, Themes } from './Css.js';
import { EventsUI } from './EventsUI.js';
import { fileFormDataFactory, Input, InputFile } from './Input.js';
import { loggerFactory } from './Logger.js';
import { Modal, renderViewTitle } from './Modal.js';
import { NotificationManager } from './NotificationManager.js';
import { RouterEvents } from './Router.js';
import { Translate } from './Translate.js';
import { Validator } from './Validator.js';
import { copyData, downloadFile, s } from './VanillaJs.js';
import { getProxyPath, getQueryParams, setPath, setQueryParams, listenQueryParamsChange } from './Router.js';
const logger = loggerFactory(import.meta);
class LoadFolderRenderer {
eGui;
async init(params) {
console.log('LoadFolderRenderer created', params);
// params.data._id
this.eGui = document.createElement('div');
this.eGui.innerHTML = html`<i class="fas fa-folder"></i> ${params.data.location}`;
}
getGui() {
return this.eGui;
}
refresh(params) {
console.log('LoadFolderRenderer refreshed', params);
return true;
}
}
class LoadFileNameRenderer {
eGui;
async init(params) {
console.log('LoadFileNameRenderer created', params);
// params.data._id
this.eGui = document.createElement('div');
this.eGui.innerHTML = html`<i class="fas fa-file"></i> ${params.data.name}`;
}
getGui() {
return this.eGui;
}
refresh(params) {
console.log('LoadFileNameRenderer refreshed', params);
return true;
}
}
class FolderHeaderComp {
eGui;
async init(params) {
console.log('FolderHeaderComp created', params);
// params.data._id
this.eGui = document.createElement('div');
this.eGui.innerHTML = html`<i class="fas fa-file"></i>`;
}
getGui() {
return this.eGui;
}
refresh(params) {
console.log('FolderHeaderComp refreshed', params);
return true;
}
}
class FileExplorer {
static Api = {};
static async instance(options = { idModal: '' }) {
const { idModal } = options;
FileExplorer.Api[idModal] = options;
const gridFolderId = 'folder-explorer-grid';
const gridFileId = 'file-explorer-grid';
const idDropFileInput = 'file-explorer';
let formBodyFiles;
const query = getQueryParams();
let location = query?.location ? FileExplorer.locationFormat({ f: query }) : '/';
let files, folders, documentId, documentInstance;
// Simple pagination state
const PAGE_SIZE = 5;
let currentPage = query?.page ? parseInt(query.page) - 1 : 0;
if (currentPage < 0) currentPage = 0;
let displayedFiles = [];
// Search filter state - initialize from URL query param
let searchFilters = {
title: query?.title || '',
mdFile: query?.mdFile || '',
file: query?.file || '',
};
let filteredFiles = [];
let isProcessingQueryChange = false; // Prevent recursion during URL sync
const queryParamsListenerId = `file-explorer-${idModal}`;
const cleanData = () => {
files = [];
folders = [];
documentId = '';
documentInstance = [];
};
cleanData();
const applySearchFilter = () => {
filteredFiles = files;
};
const updatePaginationUI = () => {
const paginationInfo = s(`.file-explorer-pagination-info`);
const prevBtn = s(`.file-explorer-prev-btn`);
const nextBtn = s(`.file-explorer-next-btn`);
if (paginationInfo) {
const totalPages = Math.ceil(filteredFiles.length / PAGE_SIZE);
const showing =
searchFilters.title || searchFilters.mdFile || searchFilters.file
? `${filteredFiles.length}/${files.length}`
: `${files.length}`;
paginationInfo.textContent = `${currentPage + 1} / ${totalPages || 1} (${showing} files)`;
}
if (prevBtn) {
prevBtn.disabled = currentPage === 0;
prevBtn.style.opacity = currentPage === 0 ? '0.5' : '1';
prevBtn.style.cursor = currentPage === 0 ? 'not-allowed' : 'pointer';
}
if (nextBtn) {
const isDisabled = (currentPage + 1) * PAGE_SIZE >= filteredFiles.length;
nextBtn.disabled = isDisabled;
nextBtn.style.opacity = isDisabled ? '0.5' : '1';
nextBtn.style.cursor = isDisabled ? 'not-allowed' : 'pointer';
}
};
const getPagedFiles = () => {
const start = currentPage * PAGE_SIZE;
const end = start + PAGE_SIZE;
return filteredFiles.slice(start, end);
};
FileExplorer.Api[idModal].displayList = async () => {
if (!s(`.${idModal}`)) return;
const query = getQueryParams();
location = query?.location ? FileExplorer.locationFormat({ f: query }) : '/';
s(`.file-explorer-query-nav`).value = location;
// Sync search filters from URL
searchFilters = {
title: query?.title || '',
mdFile: query?.mdFile || '',
file: query?.file || '',
};
if (s(`.file-explorer-search-title`)) s(`.file-explorer-search-title`).value = searchFilters.title;
if (s(`.file-explorer-search-md-file`)) s(`.file-explorer-search-md-file`).value = searchFilters.mdFile;
if (s(`.file-explorer-search-file`)) s(`.file-explorer-search-file`).value = searchFilters.file;
const format = FileExplorer.documentDataFormat({ document: documentInstance, location, searchFilters });
files = format.files;
folders = format.folders;
applySearchFilter();
const queryPage = query?.page ? parseInt(query.page) - 1 : 0;
currentPage = queryPage >= 0 ? queryPage : 0;
displayedFiles = getPagedFiles();
AgGrid.grids[gridFileId].setGridOption('rowData', displayedFiles);
AgGrid.grids[gridFolderId].setGridOption('rowData', folders);
updatePaginationUI();
};
FileExplorer.Api[idModal].updateData = async (optionsUpdate = { display: false }) => {
if (!s(`.${idModal}`)) return;
if (Auth.getToken()) {
try {
const { status, data: responseData } = await DocumentService.get({
params: {
searchTitle: searchFilters.title,
searchMdFile: searchFilters.mdFile,
searchFile: searchFilters.file,
location,
},
});
// Handle both old format (array) and new format with pagination
const document = Array.isArray(responseData) ? responseData : responseData.data || [];
const format = FileExplorer.documentDataFormat({ document, location, searchFilters });
files = format.files;
documentId = format.documentId;
folders = format.folders;
documentInstance = document;
applySearchFilter();
} catch (error) {
logger.error(error);
NotificationManager.Push({
html: error.message,
status: 'error',
});
}
} else cleanData();
setTimeout(async () => {
if (s(`.${idModal}`) && optionsUpdate && optionsUpdate.display) await FileExplorer.Api[idModal].displayList();
});
};
RouterEvents['file-explorer'] = ({ path, pushPath, route }) => {
if (route === 'cloud')
setTimeout(async () => {
// await this.Api[idModal].updateData();
await FileExplorer.Api[idModal].displayList();
});
};
// Listen for query param changes (browser back/forward navigation)
listenQueryParamsChange({
id: queryParamsListenerId,
event: async (queryParams) => {
if (!s(`.${idModal}`)) return;
if (isProcessingQueryChange) return;
const tab = queryParams?.tab || '';
if (tab === 'upload') {
s(`.file-explorer-nav`).style.display = 'none';
s(`.file-explorer-uploader`).style.display = 'block';
} else {
s(`.file-explorer-nav`).style.display = 'block';
s(`.file-explorer-uploader`).style.display = 'none';
}
const page = queryParams?.page ? parseInt(queryParams.page) - 1 : 0;
if (page !== currentPage) {
currentPage = page >= 0 ? page : 0;
displayedFiles = getPagedFiles();
if (AgGrid.grids[gridFileId]) {
AgGrid.grids[gridFileId].setGridOption('rowData', displayedFiles);
}
updatePaginationUI();
}
const newFilters = {
title: queryParams?.title || '',
mdFile: queryParams?.mdFile || '',
file: queryParams?.file || '',
};
if (
newFilters.title !== searchFilters.title ||
newFilters.mdFile !== searchFilters.mdFile ||
newFilters.file !== searchFilters.file
) {
isProcessingQueryChange = true;
searchFilters = newFilters;
if (s(`.file-explorer-search-title`)) s(`.file-explorer-search-title`).value = searchFilters.title;
if (s(`.file-explorer-search-md-file`)) s(`.file-explorer-search-md-file`).value = searchFilters.mdFile;
if (s(`.file-explorer-search-file`)) s(`.file-explorer-search-file`).value = searchFilters.file;
await FileExplorer.Api[idModal].updateData({ display: true });
setTimeout(() => {
isProcessingQueryChange = false;
}, 100);
}
},
});
// Pagination button event handlers
setTimeout(() => {
EventsUI.onClick(`.file-explorer-prev-btn`, (e) => {
e.preventDefault();
if (currentPage > 0) {
setQueryParams({ page: currentPage }, { replace: false });
}
});
EventsUI.onClick(`.file-explorer-next-btn`, (e) => {
e.preventDefault();
if ((currentPage + 1) * PAGE_SIZE < filteredFiles.length) {
setQueryParams({ page: currentPage + 2 }, { replace: false });
}
});
// Search input handlers
let searchTimeout;
const setupSearchInput = (selector, key) => {
const el = s(selector);
if (el) {
if (searchFilters[key]) el.value = searchFilters[key];
el.addEventListener('input', (e) => {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(async () => {
const val = e.target.value.trim();
if (val === searchFilters[key]) return;
isProcessingQueryChange = true;
searchFilters[key] = val;
await FileExplorer.Api[idModal].updateData({ display: true });
const queryParams = {};
if (searchFilters.title) queryParams.title = searchFilters.title;
if (searchFilters.mdFile) queryParams.mdFile = searchFilters.mdFile;
if (searchFilters.file) queryParams.file = searchFilters.file;
if (!searchFilters.title) queryParams.title = null;
if (!searchFilters.mdFile) queryParams.mdFile = null;
if (!searchFilters.file) queryParams.file = null;
queryParams.page = 1;
setQueryParams(queryParams, { replace: false });
setTimeout(() => {
isProcessingQueryChange = false;
}, 100);
}, 300);
});
el.addEventListener('keydown', (e) => {
if (e.key === 'Enter') e.preventDefault();
});
}
};
setupSearchInput(`.file-explorer-search-title`, 'title');
setupSearchInput(`.file-explorer-search-md-file`, 'mdFile');
setupSearchInput(`.file-explorer-search-file`, 'file');
// Submit search button
EventsUI.onClick(`.file-explorer-search-submit`, async (e) => {
e.preventDefault();
clearTimeout(searchTimeout);
const titleVal = s(`.file-explorer-search-title`)?.value.trim() || '';
const mdFileVal = s(`.file-explorer-search-md-file`)?.value.trim() || '';
const fileVal = s(`.file-explorer-search-file`)?.value.trim() || '';
searchFilters = { title: titleVal, mdFile: mdFileVal, file: fileVal };
isProcessingQueryChange = true;
const queryParams = {};
if (searchFilters.title) queryParams.title = searchFilters.title;
if (searchFilters.mdFile) queryParams.mdFile = searchFilters.mdFile;
if (searchFilters.file) queryParams.file = searchFilters.file;
if (!searchFilters.title) queryParams.title = null;
if (!searchFilters.mdFile) queryParams.mdFile = null;
if (!searchFilters.file) queryParams.file = null;
queryParams.page = 1;
setQueryParams(queryParams, { replace: false });
await FileExplorer.Api[idModal].updateData({ display: true });
setTimeout(() => {
isProcessingQueryChange = false;
}, 100);
});
// Clear search button
EventsUI.onClick(`.file-explorer-search-clear`, (e) => {
e.preventDefault();
if (!searchFilters.title && !searchFilters.mdFile && !searchFilters.file) return;
isProcessingQueryChange = true;
if (s(`.file-explorer-search-title`)) s(`.file-explorer-search-title`).value = '';
if (s(`.file-explorer-search-md-file`)) s(`.file-explorer-search-md-file`).value = '';
if (s(`.file-explorer-search-file`)) s(`.file-explorer-search-file`).value = '';
searchFilters = { title: '', mdFile: '', file: '' };
applySearchFilter();
currentPage = 0;
displayedFiles = getPagedFiles();
AgGrid.grids[gridFileId].setGridOption('rowData', displayedFiles);
updatePaginationUI();
setQueryParams({ title: null, mdFile: null, file: null, page: 1 }, { replace: false });
setTimeout(() => {
isProcessingQueryChange = false;
}, 100);
});
});
setTimeout(async () => {
FileExplorer.Api[idModal].updateData({ display: true });
const formData = [
{
model: 'location',
id: `file-explorer-query-nav`,
rules: [], // { type: 'isEmpty' }
},
];
const validators = await Validator.instance(formData);
EventsUI.onClick(`.btn-input-file-explorer`, async (e) => {
e.preventDefault();
// Check authentication before upload
if (!Auth.getToken()) {
return NotificationManager.Push({
html: Translate.instance(`error-user-not-authenticated`),
status: 'error',
});
}
const { errorMessage } = await validators();
if (!formBodyFiles)
return NotificationManager.Push({
html: Translate.instance(`warning-upload-no-selects-file`),
status: 'warning',
});
if (errorMessage) return;
let fileData;
{
const { status, data } = await FileService.post({ body: formBodyFiles });
if (status === 'error' || !data) {
return NotificationManager.Push({
html: Translate.instance(`error-upload-file`),
status: 'error',
});
}
fileData = data;
}
{
// const body = {};
// for (const inputData of formData) {
// if ('model' in inputData) body[inputData.model] = s(`.${inputData.id}`).value;
// }
location = FileExplorer.locationFormat({ f: { location: s(`.file-explorer-query-nav`).value } });
let status = 'success';
for (const file of fileData) {
const result = await DocumentService.post({
body: {
fileId: file._id,
location,
title: file.name,
},
});
if (result.status === 'success') documentInstance.push({ ...result.data, fileId: file });
else if (status !== 'error') status = 'error';
}
const format = FileExplorer.documentDataFormat({ document: documentInstance, location });
files = format.files;
folders = format.folders;
applySearchFilter();
currentPage = 0;
displayedFiles = getPagedFiles();
AgGrid.grids[gridFileId].setGridOption('rowData', displayedFiles);
AgGrid.grids[gridFolderId].setGridOption('rowData', folders);
updatePaginationUI();
NotificationManager.Push({
html: Translate.instance(`${status}-upload-file`),
status,
});
if (status === 'success') {
// Clear the file input
s(`.btn-clear-input-file-${idDropFileInput}`).click();
// Switch to explorer view with the uploaded location
setQueryParams({ tab: null, location: location }, { replace: false });
// Show explorer view, hide upload view
s(`.file-explorer-nav`).style.display = 'block';
s(`.file-explorer-uploader`).style.display = 'none';
s(`.file-explorer-query-nav`).value = location;
}
}
});
EventsUI.onClick(`.btn-input-go-explorer`, async (e) => {
e.preventDefault();
const newLocation = FileExplorer.locationFormat({ f: { location: s(`.file-explorer-query-nav`).value } });
if (newLocation === location) return;
location = newLocation;
setPath(`${window.location.pathname}?location=${location}`);
s(`.file-explorer-query-nav`).value = location;
// const format = this.documentDataFormat({ document: documentInstance, location });
await FileExplorer.Api[idModal].updateData({ display: true });
});
EventsUI.onClick(`.btn-input-home-directory`, async (e) => {
e.preventDefault();
if (getQueryParams()?.tab === 'upload') {
setQueryParams({ tab: null }, { replace: false });
return;
}
let newLocation = '/';
if (newLocation === location) return;
location = newLocation;
setPath(`${window.location.pathname}?location=${location}`);
s(`.file-explorer-query-nav`).value = location;
const format = FileExplorer.documentDataFormat({ document: documentInstance, location });
files = format.files;
folders = format.folders;
applySearchFilter();
currentPage = 0;
displayedFiles = getPagedFiles();
AgGrid.grids[gridFileId].setGridOption('rowData', displayedFiles);
AgGrid.grids[gridFolderId].setGridOption('rowData', folders);
updatePaginationUI();
});
EventsUI.onClick(`.btn-input-copy-directory`, async (e) => {
e.preventDefault();
await copyData(window.location.href);
NotificationManager.Push({
html: Translate.instance('success-copy-data'),
status: 'success',
});
});
EventsUI.onClick(`.btn-input-back-explorer`, async (e) => {
e.preventDefault();
window.history.back();
});
EventsUI.onClick(`.btn-input-forward-explorer`, async (e) => {
e.preventDefault();
window.history.forward();
});
EventsUI.onClick(`.btn-input-upload-file`, async (e) => {
e.preventDefault();
setQueryParams({ tab: 'upload' }, { replace: false });
});
});
class LoadFileActionsRenderer {
eGui;
async init(params) {
console.log('LoadFileActionsRenderer created', params);
// params.data._id
this.eGui = document.createElement('div');
const isPublic = params.data.isPublic;
const toggleId = `toggle-public-${params.data._id}`;
const hasGenericFile = !!params.data.hasGenericFile;
const hasMdFile = !!params.data.hasMdFile;
this.eGui.innerHTML = html`
<div class="fl">
${await BtnIcon.instance({
class: `in fll management-table-btn-mini btn-file-download-${params.data._id}${!hasGenericFile ? ' btn-disabled' : ''}`,
label: html` <i class="fas fa-download"></i>`,
type: 'button',
})}
${await BtnIcon.instance({
class: `in fll management-table-btn-mini btn-file-delete-${params.data._id}`,
label: html` <i class="fa-solid fa-circle-xmark"></i>`,
type: 'button',
})}
${await BtnIcon.instance({
class: `in fll management-table-btn-mini btn-file-view-${params.data._id}`,
label: html` <i class="fas fa-eye"></i>`,
type: 'button',
})}
${await BtnIcon.instance({
class: `in fll management-table-btn-mini btn-file-copy-content-link-${params.data._id}${!hasGenericFile ? ' btn-disabled' : ''}`,
label: html`<i class="fas fa-copy"></i>`,
type: 'button',
})}
${await BtnIcon.instance({
class: `in fll management-table-btn-mini btn-file-copy-md-link-${params.data._id}${!hasMdFile ? ' btn-disabled' : ''}`,
label: html`<i class="fas fa-file-code"></i>`,
type: 'button',
})}
${await BtnIcon.instance({
class: `in fll management-table-btn-mini btn-file-edit-${params.data._id}`,
label: html`<i class="fas fa-edit"></i>`,
type: 'button',
})}
${await BtnIcon.instance({
class: `in fll management-table-btn-mini ${toggleId}`,
label: isPublic
? html`<i class="fas fa-globe" style="color: #4caf50;"></i>`
: html`<i class="fas fa-lock" style="color: #9e9e9e;"></i>`,
type: 'button',
})}
</div>
`;
setTimeout(() => {
const uri = `${getProxyPath()}content/?cid=${params.data._id}`;
const url = `${window.location.origin}${uri}`;
const originObj = documentInstance.find((d) => d._id === params.data._id);
const blobUri =
originObj && originObj.fileId
? getApiBaseUrl({ id: originObj.fileId._id, endpoint: 'file/blob' })
: undefined;
const mdBlobUri =
originObj && originObj.mdFileId
? getApiBaseUrl({ id: originObj.mdFileId._id, endpoint: 'file/blob' })
: undefined;
if (!originObj) {
s(`.btn-file-view-${params.data._id}`).classList.add('hide');
s(`.btn-file-copy-content-link-${params.data._id}`).classList.add('hide');
}
// Disable download button if no generic file
if (!hasGenericFile) {
const dlBtn = s(`.btn-file-download-${params.data._id}`);
if (dlBtn) {
dlBtn.style.opacity = '0.4';
dlBtn.style.cursor = 'not-allowed';
dlBtn.style.pointerEvents = 'none';
}
}
// Disable copy generic file link button if no generic file
if (!hasGenericFile) {
const copyBtn = s(`.btn-file-copy-content-link-${params.data._id}`);
if (copyBtn) {
copyBtn.style.opacity = '0.4';
copyBtn.style.cursor = 'not-allowed';
copyBtn.style.pointerEvents = 'none';
}
}
// Disable copy md file link button if no md file
if (!hasMdFile) {
const mdCopyBtn = s(`.btn-file-copy-md-link-${params.data._id}`);
if (mdCopyBtn) {
mdCopyBtn.style.opacity = '0.4';
mdCopyBtn.style.cursor = 'not-allowed';
mdCopyBtn.style.pointerEvents = 'none';
}
}
EventsUI.onClick(`.btn-file-view-${params.data._id}`, async (e) => {
e.preventDefault();
if (location.href !== url) {
setPath(uri);
s(`.main-btn-content`).click();
}
});
EventsUI.onClick(`.btn-file-copy-content-link-${params.data._id}`, async (e) => {
e.preventDefault();
if (!hasGenericFile || !blobUri) return;
await copyData(blobUri);
NotificationManager.Push({
html: Translate.instance('success-copy-data'),
status: 'success',
});
});
EventsUI.onClick(`.btn-file-copy-md-link-${params.data._id}`, async (e) => {
e.preventDefault();
if (!hasMdFile || !mdBlobUri) return;
await copyData(mdBlobUri);
NotificationManager.Push({
html: Translate.instance('success-copy-data'),
status: 'success',
});
});
EventsUI.onClick(`.btn-file-download-${params.data._id}`, async (e) => {
e.preventDefault();
if (!hasGenericFile) return;
try {
// Use FileService with blob/ prefix for blob fetching
const { data: blobArray, status } = await FileService.get({ id: `blob/${params.data.fileId}` });
if (status === 'success' && blobArray && blobArray[0]) {
downloadFile(blobArray[0], params.data.name);
} else {
throw new Error('Failed to fetch file blob');
}
} catch (error) {
logger.error('Download failed:', error);
NotificationManager.Push({
html: 'Download failed',
status: 'error',
});
}
});
EventsUI.onClick(
`.btn-file-delete-${params.data._id}`,
async (e) => {
e.preventDefault();
{
const confirmResult = await Modal.RenderConfirm({
html: async () => {
return html`
<div class="in section-mp" style="text-align: center">
${Translate.instance('confirm-delete-item')}
<br />
"${params.data.title}"
</div>
`;
},
id: `delete-${params.data._id}`,
});
if (confirmResult.status !== 'confirm') return;
const { data, status, message } = await FileService.delete({
id: params.data.fileId,
});
NotificationManager.Push({
html: status,
status,
});
if (status === 'error') return;
}
const { data, status, message } = await DocumentService.delete({
id: params.data._id,
});
NotificationManager.Push({
html: status,
status,
});
if (status === 'error') return;
documentInstance = documentInstance.filter((f) => f._id !== params.data._id);
const format = FileExplorer.documentDataFormat({ document: documentInstance, location });
files = format.files;
folders = format.folders;
applySearchFilter();
// AgGrid.grids[gridFileId].setGridOption('rowData', files);
// const selectedData = gridApi.getSelectedRows();
AgGrid.grids[gridFileId].applyTransaction({ remove: [params.data] });
AgGrid.grids[gridFolderId].setGridOption('rowData', folders);
updatePaginationUI();
},
{ context: 'modal' },
);
// Toggle public/private status
EventsUI.onClick(
`.${toggleId}`,
async (e) => {
e.preventDefault();
// If document is currently private, show confirmation before making public
if (!params.data.isPublic) {
const confirmResult = await Modal.RenderConfirm({
html: async () => {
return html`
<div class="in section-mp" style="text-align: center">
${Translate.instance('confirm-make-public')}
<br />
"${params.data.title}"
</div>
`;
},
id: `confirm-toggle-public-${params.data._id}`,
});
if (confirmResult.status !== 'confirm') return;
}
try {
const { data, status } = await DocumentService.patch({
id: params.data._id,
action: 'toggle-public',
});
if (status === 'success') {
// Update local data
params.data.isPublic = data.isPublic;
// Update documentInstance
const docIndex = documentInstance.findIndex((d) => d._id === params.data._id);
if (docIndex !== -1) {
documentInstance[docIndex].isPublic = data.isPublic;
}
// Refresh the isPublic column cell in the grid
const rowNode = AgGrid.grids[gridFileId].getRowNode(params.node.id);
if (rowNode) {
rowNode.setDataValue('isPublic', data.isPublic);
}
// Update button icon
const btnElement = s(`.${toggleId}`);
if (btnElement) {
const iconElement = btnElement.querySelector('i');
if (iconElement) {
if (data.isPublic) {
iconElement.className = 'fas fa-globe';
iconElement.style.color = '#4caf50';
} else {
iconElement.className = 'fas fa-lock';
iconElement.style.color = '#9e9e9e';
}
}
}
NotificationManager.Push({
html: data.isPublic
? Translate.instance('document-now-public')
: Translate.instance('document-now-private'),
status: 'success',
});
} else {
throw new Error('Failed to toggle public status');
}
} catch (error) {
logger.error('Toggle public failed:', error);
NotificationManager.Push({
html: Translate.instance('error-toggle-public'),
status: 'error',
});
}
},
{ context: 'modal' },
);
// Edit document button
EventsUI.onClick(
`.btn-file-edit-${params.data._id}`,
async (e) => {
e.preventDefault();
// Get the original document data from documentInstance
const originDoc = documentInstance.find((d) => d._id === params.data._id);
const editModalId = `edit-doc-${params.data._id}`;
// Check file existence for proper UX
const hasMdFile = !!(originDoc && originDoc.mdFileId);
const hasGenericFile = !!(originDoc && originDoc.fileId);
const mdFileMimetype = hasMdFile ? originDoc.mdFileId.mimetype : '';
const genericFileMimetype = hasGenericFile ? originDoc.fileId.mimetype : '';
const editFormHtml = async () => {
return html`
<div class="in edit-document-form" style="max-width: 600px; margin: 0 auto; padding: 20px;">
<!-- Header -->
<div
class="in"
style="text-align: center; margin-bottom: 25px; padding-bottom: 15px; border-bottom: 1px solid rgba(128,128,128,0.3);"
>
<p style="color: #888; font-size: 14px; margin: 0;">
${Translate.instance('editing')}: <strong style="color: inherit;">${params.data.title}</strong>
</p>
</div>
<!-- Document Title -->
<div class="in section-mp" style="margin-bottom: 20px;">
${await Input.instance({
id: `edit-doc-title-${params.data._id}`,
type: 'text',
label: html`<i class="fas fa-heading"></i> ${Translate.instance('doc-title')}`,
containerClass: 'in section-mp input-container-width',
placeholder: true,
value: params.data.title || '',
})}
</div>
<!-- MD File Name -->
<div class="in section-mp" style="margin-bottom: 20px;">
${hasMdFile
? await Input.instance({
id: `edit-doc-md-file-${params.data._id}`,
type: 'text',
label: html`<i class="fas fa-file-code"></i> ${Translate.instance('md-file-name')}
<span style="font-size: 11px; color: #888; margin-left: 8px;">(${mdFileMimetype})</span>`,
containerClass: 'in section-mp input-container-width',
placeholder: true,
value: params.data.mdFileName || '',
})
: html`
<div class="in section-mp input-container-width" style="opacity: 0.6;">
<label style="display: block; margin-bottom: 5px;">
<i class="fas fa-file-code"></i> ${Translate.instance('md-file-name')}
</label>
<div
style="padding: 10px 12px; border: 1px dashed rgba(128,128,128,0.5); border-radius: 4px; color: #888; font-style: italic;"
>
<i class="fas fa-info-circle"></i> ${Translate.instance('no-md-file-attached')}
</div>
</div>
`}
</div>
<!-- Generic File Name -->
<div class="in section-mp" style="margin-bottom: 20px;">
${hasGenericFile
? await Input.instance({
id: `edit-doc-file-${params.data._id}`,
type: 'text',
label: html`<i class="fas fa-file"></i> ${Translate.instance('generic-file-name')}
<span style="font-size: 11px; color: #888; margin-left: 8px;"
>(${genericFileMimetype})</span
>`,
containerClass: 'in section-mp input-container-width',
placeholder: true,
value: params.data.fileName || '',
})
: html`
<div class="in section-mp input-container-width" style="opacity: 0.6;">
<label style="display: block; margin-bottom: 5px;">
<i class="fas fa-file"></i> ${Translate.instance('generic-file-name')}
</label>
<div
style="padding: 10px 12px; border: 1px dashed rgba(128,128,128,0.5); border-radius: 4px; color: #888; font-style: italic;"
>
<i class="fas fa-info-circle"></i> ${Translate.instance('no-generic-file-attached')}
</div>
</div>
`}
</div>
<!-- Location -->
<div class="in section-mp" style="margin-bottom: 25px;">
${await Input.instance({
id: `edit-doc-location-${params.data._id}`,
type: 'text',
label: html`<i class="fas fa-folder"></i> ${Translate.instance('location')}`,
containerClass: 'in section-mp input-container-width',
placeholder: true,
value: params.data.location || '/',
})}
</div>
<!-- Buttons -->
<div
class="fl"
style="margin-top: 30px; border-top: 1px solid rgba(128,128,128,0.3); padding-top: 20px;"
>
<div class="in fll" style="width: 50%; padding: 5px;">
${await BtnIcon.instance({
class: `in wfa btn-edit-doc-cancel-${params.data._id}`,
label: html`<i class="fas fa-times"></i> ${Translate.instance('cancel')}`,
type: 'button',
})}
</div>
<div class="in fll" style="width: 50%; padding: 5px;">
${await BtnIcon.instance({
class: `in wfa btn-edit-doc-submit-${params.data._id}`,
label: html`<i class="fas fa-save"></i> ${Translate.instance('save')}`,
type: 'button',
})}
</div>
</div>
</div>
`;
};
const { barConfig } = await Themes[Css.currentTheme]();
await Modal.instance({
id: editModalId,
barConfig,
title: renderViewTitle({
icon: html`<i class="fas fa-edit"></i>`,
text: Translate.instance('edit-document'),
}),
html: editFormHtml,
handleType: 'bar',
maximize: true,
mode: 'view',
slideMenu: 'modal-menu',
RouterInstance: Modal.Data[options.idModal].options.RouterInstance,
barMode: Modal.Data[options.idModal].options.barMode,
});
// Handle submit button
setTimeout(() => {
EventsUI.onClick(
`.btn-edit-doc-submit-${params.data._id}`,
async (ev) => {
ev.preventDefault();
const newTitle = s(`.edit-doc-title-${params.data._id}`).value.trim();
const newLocation = s(`.edit-doc-location-${params.data._id}`).value.trim();
// Get file names only if files exist
const newMdFileName = hasMdFile ? s(`.edit-doc-md-file-${params.data._id}`)?.value.trim() : null;
const newFileName = hasGenericFile ? s(`.edit-doc-file-${params.data._id}`)?.value.trim() : null;
if (!newTitle) {
NotificationManager.Push({
html: Translate.instance('error-title-required'),
status: 'error',
});
return;
}
const formattedLocation = FileExplorer.locationFormat({ f: { location: newLocation || '/' } });
try {
const updateBody = {
title: newTitle,
location: formattedLocation,
};
// Preserve existing fields from the original document
if (originDoc) {
if (originDoc.fileId) updateBody.fileId = originDoc.fileId._id || originDoc.fileId;
if (originDoc.mdFileId) updateBody.mdFileId = originDoc.mdFileId._id || originDoc.mdFileId;
if (originDoc.tags) updateBody.tags = originDoc.tags;
if (typeof originDoc.isPublic !== 'undefined') updateBody.isPublic = originDoc.isPublic;
}
// Include file name updates if files exist and names changed
if (hasMdFile && newMdFileName && newMdFileName !== params.data.mdFileName) {
updateBody.mdFileName = newMdFileName;
}
if (hasGenericFile && newFileName && newFileName !== params.data.fileName) {
updateBody.fileName = newFileName;
}
const { data, status } = await DocumentService.put({
id: params.data._id,
body: updateBody,
});
if (status === 'success') {
// Check if location changed
const locationChanged = formattedLocation !== params.data.location;
// Update local data
params.data.title = newTitle;
params.data.name = newTitle;
params.data.location = formattedLocation;
if (hasMdFile && newMdFileName) params.data.mdFileName = newMdFileName;
if (hasGenericFile && newFileName) params.data.fileName = newFileName;
// Update documentInstance
const docIndex = documentInstance.findIndex((d) => d._id === params.data._id);
if (docIndex !== -1) {
documentInstance[docIndex].title = newTitle;
documentInstance[docIndex].location = formattedLocation;
// Update file names in the referenced file objects
if (hasMdFile && newMdFileName && documentInstance[docIndex].mdFileId) {
documentInstance[docIndex].mdFileId.name = newMdFileName;
}
if (hasGenericFile && newFileName && documentInstance[docIndex].fileId) {
documentInstance[docIndex].fileId.name = newFileName;
}
}
// Refresh the grid with new location
const format = FileExplorer.documentDataFormat({ document: documentInstance, location });
files = format.files;
folders = format.folders;
applySearchFilter();
currentPage = 0;
displayedFiles = getPagedFiles();
AgGrid.grids[gridFileId].setGridOption('rowData', displayedFiles);
AgGrid.grids[gridFolderId].setGridOption('rowData', folders);
updatePaginationUI();
NotificationManager.Push({
html: Translate.instance('success-update-document'),
status: 'success',
});
// If location changed, navigate to the new location
if (!s(`.file-explorer-query-nav`)) s(`.main-btn-cloud`).click();
if (locationChanged)
setTimeout(() => {
location = formattedLocation;
setPath(`${window.location.pathname}?location=${location}`);
s(`.file-explorer-query-nav`).value = location;
});
Modal.removeModal(editModalId);
} else {
throw new Error('Failed to update document');
}
} catch (error) {
logger.error('Update document failed:', error);
NotificationManager.Push({
html: Translate.instance('error-update-document'),
status: 'error',
});
}
},
{ context: 'modal' },
);
// Handle cancel button
EventsUI.onClick(
`.btn-edit-doc-cancel-${params.data._id}`,
async (ev) => {
ev.preventDefault();
Modal.removeModal(editModalId);
},
{ context: 'modal' },
);
});
},
{ context: 'modal' },
);
});
}
getGui() {
return this.eGui;
}
refresh(params) {
console.log('LoadFileActionsRenderer refreshed', params);
return true;
}
}
class LoadFolderActionsRenderer {
eGui;
async init(params) {
console.log('LoadFolderActionsRenderer created', params);
// params.data._id
const id = params.data.locationId;
this.eGui = document.createElement('div');
this.eGui.innerHTML = html`
<div class="fl">
${await BtnIcon.instance({
class: `in fll management-table-btn-mini btn-folder-delete-${id}`,
label: html` <i class="fa-solid fa-circle-xmark"></i>`,
type: 'button',
})}
</div>
`;
setTimeout(() => {
EventsUI.onClick(
`.btn-folder-delete-${id}`,
async (e) => {
const confirmResult = await Modal.RenderConfirm({
html: async () => {
return html`
<div class="in section-mp" style="text-align: center">
${Translate.instance('confirm-delete-item')}
<br />
"${params.data.location}"
</div>
`;
},
id: `delete-${id}`,
});
if (confirmResult.status !== 'confirm') return;
e.preventDefault();
const idFilesDelete = [];
for (const file of documentInstance.filter(
(f) => FileExplorer.locationFormat({ f }) === params.data.location,
)) {
{
const { data, status, message } = await FileService.delete({
id: file.fileId._id,
});
}
{
idFilesDelete.push(file._id);
const { data, status, message } = await DocumentService.delete({
id: file._id,
});
}
}
NotificationManager.Push({
html: Translate.instance('success-delete'),
status: 'success',
});
documentInstance = documentInstance.filter((f) => !idFilesDelete.includes(f._id));
const format = FileExplorer.documentDataFormat({ document: documentInstance, location });
files = format.files;
folders = format.folders;
applySearchFilter();
currentPage = 0;
displayedFiles = getPagedFiles();
AgGrid.grids[gridFileId].setGridOption('rowData', displayedFiles);
AgGrid.grids[gridFolderId].setGridOption('rowData', folders);
updatePaginationUI();
},
{ context: 'modal' },
);
});
}
getGui() {
return this.eGui;
}
refresh(params) {
console.log('LoadFolderActionsRenderer refreshed', params);
return true;
}
}
return html`
<form>
<div class="fl">
${await BtnIcon.instance({
class: 'in fll management-table-btn-mini btn-input-home-directory',
label: html`<i class="fas fa-home"></i>
<!-- ${Translate.instance('home-directory')} -->`,
type: 'button',
})}
${await BtnIcon.instance({
class: 'in fll management-table-btn-mini btn-input-back-explorer',
label: html` <i class="fa-solid fa-circle-left"></i>
<!-- ${Translate.instance('go')} -->`,
type: 'button',
})}
${await BtnIcon.instance({
class: 'in fll management-table-btn-mini btn-input-forward-explorer',
label: html` <i class="fa-solid fa-circle-right"></i>
<!-- ${Translate.instance('go')} -->`,
type: 'button',
})}
${await BtnIcon.instance({
class: 'in fll management-table-btn-mini btn-input-go-explorer',
label: html`<i class="fas fa-sync-alt"></i>
<!-- ${Translate.instance('go')} -->`,
type: 'submit',
})}
${await BtnIcon.instance({
class: 'in fll management-table-btn-mini btn-input-copy-directory',
label: html`<i class="fas fa-copy"></i>
<!-- ${Translate.instance('home-directory')} -->`,
type: 'button',
})}
${await BtnIcon.instance({
class: 'in fll management-table-btn-mini btn-input-upload-file',
label: html`<i class="fa-solid fa-cloud-arrow-up"></i>
<!-- ${Translate.instance('home-directory')} -->`,
type: 'button',
})}
</div>
<div class="in">
${await Input.instance({
id: `file-explorer-query-nav`,
type: 'text',
label: html`<i class="fab fa-wpexplorer"></i> ${Translate.instance('current-path')}`,
containerClass: 'i