Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- Of course! This is an excellent and very achievable project for Google Apps Script. The key challenge you've faced, the script timeout, is a classic problem when dealing with large datasets in Google's ecosystem. The solution is to design a script that can pause its work and resume where it left off, running in short bursts until the job is done.
- We will build a robust, user-friendly solution that addresses all your requirements. It will feature a sophisticated user interface (UI) within your spreadsheet, making it easy for anyone at your firm to use.
- This is a single-file solution. You will only need to copy and paste the code below into the Apps Script editor for your Google Sheet.
- Instructions for Installation
- * Create a new Google Sheet. Name it something like "Case File Index".
- * Go to Extensions > Apps Script.
- * Delete any code in the Code.gs file.
- * Copy the entire code block below and paste it into the Code.gs editor.
- * Click the Save project icon (looks like a floppy disk).
- * Go back to your Google Sheet and refresh the page. A new menu named 🗂️ File Indexer should appear.
- * The first time you run a function, Google will ask for permissions. You must grant these for the script to access your Drive and Sheets.
- The Complete Code.gs Script
- // Copyright 2025
- // Developed for a legal assistant to index large Google Drive folders.
- // This script is designed to be robust, user-friendly, and handle large file counts
- // by using a resumable, batched process to avoid timeouts.
- // --- CONFIGURATION & CONSTANTS --- //
- const INDEX_SHEET_NAME = 'File Index';
- const CONFIG_SHEET_NAME = 'Configuration';
- const HEADERS = ['Parent Folder', 'File Name', 'Date Created', 'File Type', 'Link'];
- const DEFAULT_COLORS = [
- '#e0e7ff', '#fee2e2', '#d1fae5', '#fef3c7', '#f3e8ff',
- '#dbeafe', '#ffe4e6', '#cce7e0', '#fef9c3', '#ede9fe',
- '#c7d2fe', '#fecdd3', '#bbf7d0', '#fde68a', '#ddd6fe'
- ];
- const MAX_RUNTIME_MS = 5 * 60 * 1000; // 5 minutes to be safe from 6-min timeout.
- const SCRIPT_LOCK_TIMEOUT_MS = 30 * 1000; // 30 seconds.
- const SHEET_CAPACITY_THRESHOLD = 0.95; // Warn user at 95% sheet capacity.
- // --- SPREADSHEET UI & INITIALIZATION --- //
- /**
- * Creates a custom menu in the spreadsheet when the file is opened.
- * @param {object} e The event object.
- */
- function onOpen(e) {
- SpreadsheetApp.getUi()
- .createMenu('🗂️ File Indexer')
- .addItem('🚀 Open Dashboard', 'showIndexerModal')
- .addSeparator()
- .addItem('🔍 Start/Update Index Scan', 'startIndexingProcess')
- .addToUi();
- // Ensure required sheets exist
- const ss = SpreadsheetApp.getActiveSpreadsheet();
- if (!ss.getSheetByName(INDEX_SHEET_NAME)) {
- const sheet = ss.insertSheet(INDEX_SHEET_NAME);
- sheet.appendRow(HEADERS).setFrozenRows(1);
- sheet.getRange("A1:E1").setFontWeight('bold');
- }
- if (!ss.getSheetByName(CONFIG_SHEET_NAME)) {
- const configSheet = ss.insertSheet(CONFIG_SHEET_NAME);
- const protection = configSheet.protect().setDescription('Configuration sheet protected by script');
- const me = Session.getEffectiveUser();
- protection.addEditor(me);
- protection.removeEditors(protection.getEditors());
- if (protection.canDomainEdit()) {
- protection.setDomainEdit(false);
- }
- }
- }
- /**
- * Displays the main UI modal dialog.
- */
- function showIndexerModal() {
- const htmlOutput = HtmlService.createHtmlOutput(getModalHtml())
- .setWidth(900)
- .setHeight(650);
- SpreadsheetApp.getUi().showModalDialog(htmlOutput, '🗂️ File Indexer Dashboard');
- }
- // --- BACKEND FUNCTIONS CALLED FROM UI --- //
- /**
- * Fetches statistics for the dashboard landing page.
- * @returns {object} An object containing dashboard statistics.
- */
- function getDashboardStats() {
- try {
- const ss = SpreadsheetApp.getActiveSpreadsheet();
- const indexSheet = ss.getSheetByName(INDEX_SHEET_NAME);
- const configSheet = ss.getSheetByName(CONFIG_SHEET_NAME);
- const dataRange = indexSheet.getDataRange();
- const values = dataRange.getValues();
- const fileCount = Math.max(0, values.length - 1);
- const parentFolders = new Set(values.slice(1).map(row => row[0]));
- const mimeTypeCounts = values.slice(1).reduce((acc, row) => {
- const type = row[3] || 'Unknown';
- acc[type] = (acc[type] || 0) + 1;
- return acc;
- }, {});
- const maxRows = ss.getMaxRows();
- const currentRowCount = indexSheet.getLastRow();
- const capacity = currentRowCount / maxRows;
- let capacityWarning = null;
- if (capacity > SHEET_CAPACITY_THRESHOLD) {
- capacityWarning = `Warning: Sheet is at ${Math.round(capacity * 100)}% capacity (${currentRowCount} of ${maxRows} rows).`;
- }
- const folderTree = getFolderTree();
- return {
- fileCount: fileCount,
- parentFolderCount: parentFolders.size,
- mimeTypeCounts: mimeTypeCounts,
- capacityWarning: capacityWarning,
- folderTree: folderTree,
- };
- } catch (e) {
- return { error: `Could not load stats: ${e.message}` };
- }
- }
- /**
- * Retrieves the color configuration from the 'Configuration' sheet.
- * @returns {object} Key-value pairs of folder names and hex colors.
- */
- function getColorConfig() {
- try {
- const configSheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(CONFIG_SHEET_NAME);
- if (!configSheet) return {};
- const data = configSheet.getDataRange().getValues();
- return data.reduce((acc, row) => {
- if (row[0] && row[1]) acc[row[0]] = row[1];
- return acc;
- }, {});
- } catch (e) {
- return { error: `Could not load color config: ${e.message}` };
- }
- }
- /**
- * Saves the color configuration to the 'Configuration' sheet.
- * @param {object} config The configuration object to save.
- * @returns {object} A success or error message.
- */
- function saveColorConfig(config) {
- try {
- const configSheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(CONFIG_SHEET_NAME);
- configSheet.clearContents();
- const rows = Object.entries(config);
- if (rows.length > 0) {
- configSheet.getRange(1, 1, rows.length, 2).setValues(rows);
- }
- return { success: 'Color configuration saved successfully!' };
- } catch (e) {
- return { error: `Failed to save configuration: ${e.message}` };
- }
- }
- /**
- * Builds a hierarchical tree of indexed folders and their file counts.
- * This is a simplified version for the UI and reads from the already indexed sheet.
- * @returns {object} A nested object representing the folder tree.
- */
- function getFolderTree() {
- const ss = SpreadsheetApp.getActiveSpreadsheet();
- const indexSheet = ss.getSheetByName(INDEX_SHEET_NAME);
- if (indexSheet.getLastRow() <= 1) return {};
- const values = indexSheet.getRange(2, 1, indexSheet.getLastRow() - 1, 5).getValues();
- const folderData = {}; // { folderId: { name, url, count } }
- const fileData = {}; // { fileId: { parentFolderId } }
- values.forEach(row => {
- try {
- const parentFolderName = row[0];
- const fileUrl = row[4];
- if (!parentFolderName || !fileUrl) return;
- const fileIdMatch = fileUrl.match(/id=([^&]+)/);
- if (!fileIdMatch) return;
- const fileId = fileIdMatch[1];
- const file = DriveApp.getFileById(fileId);
- const parents = file.getParents();
- if (parents.hasNext()) {
- const parentFolder = parents.next();
- const parentId = parentFolder.getId();
- if (!folderData[parentId]) {
- folderData[parentId] = {
- name: parentFolder.getName(),
- url: parentFolder.getUrl(),
- count: 0
- };
- }
- folderData[parentId].count++;
- }
- } catch (e) {
- // Ignore files that may have been deleted or are inaccessible
- }
- });
- return Object.values(folderData).sort((a,b) => a.name.localeCompare(b.name));
- }
- // --- CORE INDEXING LOGIC (RESUMABLE/BATCHED) --- //
- /**
- * Initiates the indexing process.
- * This function sets up the state for a resumable, triggered execution.
- */
- function startIndexingProcess() {
- const lock = LockService.getScriptLock();
- if (!lock.tryLock(SCRIPT_LOCK_TIMEOUT_MS)) {
- SpreadsheetApp.getUi().alert('An indexing process is already running. Please wait for it to complete.');
- return;
- }
- try {
- // Clear any previous triggers to start fresh
- const allTriggers = ScriptApp.getProjectTriggers();
- for (const trigger of allTriggers) {
- if (trigger.getHandlerFunction() === 'continueIndexingProcess') {
- ScriptApp.deleteTrigger(trigger);
- }
- }
- const properties = PropertiesService.getScriptProperties();
- // Get all existing file IDs from the index to avoid duplicates
- const indexSheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(INDEX_SHEET_NAME);
- const existingFileIds = new Set();
- const idColumnValues = indexSheet.getRange("E2:E").getValues();
- idColumnValues.forEach(row => {
- if(row[0]) {
- const match = row[0].match(/id=([^&]+)/);
- if (match) existingFileIds.add(match[1]);
- }
- });
- // The user's desired search query: find all folders that are shared with them.
- // This intelligently targets collaborative folders.
- const searchQuery = 'mimeType="application/vnd.google-apps.folder" and sharedWithMe';
- const folders = DriveApp.searchFolders(searchQuery);
- const folderQueue = [];
- while(folders.hasNext()){
- folderQueue.push(folders.next().getId());
- }
- if (folderQueue.length === 0) {
- SpreadsheetApp.getUi().alert('No shared folders found matching the criteria. Nothing to index.');
- return;
- }
- // Store initial state for the batch processor
- properties.setProperties({
- 'folderQueue': JSON.stringify(folderQueue),
- 'currentFolderIndex': '0',
- 'existingFileIds': JSON.stringify(Array.from(existingFileIds)),
- 'colorConfigCache': JSON.stringify(getColorConfig()),
- 'lastAppliedColorIndex': '0'
- });
- // Start the first batch
- ScriptApp.newTrigger('continueIndexingProcess').timeBased().after(1000).create();
- SpreadsheetApp.getUi().showToast('🚀 Indexing scan has started! This may take several minutes. You can close this window.', 'Process Started', 10);
- } catch(e) {
- SpreadsheetApp.getUi().alert(`An error occurred while starting the scan: ${e.message}`);
- } finally {
- lock.releaseLock();
- }
- }
- /**
- * Processes a batch of files. This function is called by a trigger
- * and re-schedules itself until all folders are processed.
- */
- function continueIndexingProcess() {
- const lock = LockService.getScriptLock();
- if (!lock.tryLock(SCRIPT_LOCK_TIMEOUT_MS)) {
- console.warn('Skipping execution because a previous one is still running.');
- return;
- }
- const startTime = Date.now();
- const properties = PropertiesService.getScriptProperties();
- const state = properties.getProperties();
- const folderQueue = JSON.parse(state.folderQueue || '[]');
- let currentFolderIndex = parseInt(state.currentFolderIndex || '0');
- const existingFileIds = new Set(JSON.parse(state.existingFileIds || '[]'));
- try {
- if (currentFolderIndex >= folderQueue.length) {
- cleanupTriggersAndFinish();
- return;
- }
- const newRows = [];
- const ss = SpreadsheetApp.getActiveSpreadsheet();
- const indexSheet = ss.getSheetByName(INDEX_SHEET_NAME);
- while (currentFolderIndex < folderQueue.length) {
- const folderId = folderQueue[currentFolderIndex];
- const folder = DriveApp.getFolderById(folderId);
- // Recursive function to process files within this folder and its subfolders
- processFolderRecursively(folder, folder, newRows, existingFileIds, startTime);
- currentFolderIndex++;
- properties.setProperty('currentFolderIndex', currentFolderIndex.toString());
- // If we are running out of time, write what we have and reschedule
- if (Date.now() - startTime > MAX_RUNTIME_MS) {
- if (newRows.length > 0) {
- writeAndColorRows(indexSheet, newRows, state);
- }
- reschedule();
- lock.releaseLock();
- return;
- }
- }
- // Write any remaining rows from the last folder
- if (newRows.length > 0) {
- writeAndColorRows(indexSheet, newRows, state);
- }
- cleanupTriggersAndFinish();
- } catch (e) {
- console.error(`Error during indexing: ${e.message} Stack: ${e.stack}`);
- cleanupTriggersAndFinish(true); // Finish with error
- } finally {
- lock.releaseLock();
- }
- }
- /**
- * Helper function to recursively traverse folders.
- */
- function processFolderRecursively(rootFolder, currentFolder, newRows, existingFileIds, startTime) {
- if (Date.now() - startTime > MAX_RUNTIME_MS) return;
- const files = currentFolder.getFiles();
- while (files.hasNext()) {
- if (Date.now() - startTime > MAX_RUNTIME_MS) return;
- const file = files.next();
- if (!existingFileIds.has(file.getId())) {
- newRows.push([
- rootFolder.getName(),
- file.getName(),
- file.getDateCreated(),
- file.getMimeType(),
- file.getUrl()
- ]);
- existingFileIds.add(file.getId()); // Add to set to avoid duplicates in the same run
- }
- }
- const subFolders = currentFolder.getFolders();
- while (subFolders.hasNext()) {
- if (Date.now() - startTime > MAX_RUNTIME_MS) return;
- processFolderRecursively(rootFolder, subFolders.next(), newRows, existingFileIds, startTime);
- }
- }
- /**
- * Writes new rows to the sheet and applies color coding.
- */
- function writeAndColorRows(sheet, rows, state) {
- const startRow = sheet.getLastRow() + 1;
- sheet.getRange(startRow, 1, rows.length, HEADERS.length).setValues(rows);
- // Apply color coding
- const colorConfig = JSON.parse(state.colorConfigCache || '{}');
- const parentFolderColorMap = {};
- let lastAppliedColorIndex = parseInt(state.lastAppliedColorIndex || '0');
- const range = sheet.getRange(startRow, 1, rows.length, HEADERS.length);
- const backgroundColors = range.getBackgrounds();
- const folderNames = range.getValues().map(row => row[0]);
- for (let i = 0; i < folderNames.length; i++) {
- const folderName = folderNames[i];
- if (!parentFolderColorMap[folderName]) {
- if (colorConfig[folderName]) {
- parentFolderColorMap[folderName] = colorConfig[folderName];
- } else {
- parentFolderColorMap[folderName] = DEFAULT_COLORS[lastAppliedColorIndex % DEFAULT_COLORS.length];
- lastAppliedColorIndex++;
- }
- }
- backgroundColors[i] = Array(HEADERS.length).fill(parentFolderColorMap[folderName]);
- }
- range.setBackgrounds(backgroundColors);
- PropertiesService.getScriptProperties().setProperty('lastAppliedColorIndex', lastAppliedColorIndex.toString());
- }
- /**
- * Reschedules the 'continueIndexingProcess' to run again.
- */
- function reschedule() {
- ScriptApp.newTrigger('continueIndexingProcess').timeBased().after(1000).create();
- console.log('Rescheduling next batch...');
- }
- /**
- * Cleans up triggers and properties, then notifies the user.
- */
- function cleanupTriggersAndFinish(isError = false) {
- const allTriggers = ScriptApp.getProjectTriggers();
- for (const trigger of allTriggers) {
- if (trigger.getHandlerFunction() === 'continueIndexingProcess') {
- ScriptApp.deleteTrigger(trigger);
- }
- }
- PropertiesService.getScriptProperties().deleteAllProperties();
- const message = isError ? '❌ Indexing stopped due to an error. Please check logs.' : '✅ Indexing complete! All files have been processed.';
- const title = isError ? 'Error' : 'Success';
- SpreadsheetApp.getActiveSpreadsheet().toast(message, title, 10);
- console.log(message);
- }
- // --- HTML & CLIENT-SIDE JAVASCRIPT FOR MODAL --- //
- function getModalHtml() {
- return `
- <!DOCTYPE html>
- <html>
- <head>
- <base target="_top">
- <link href="https://cdnjs.cloudflare.com/ajax/libs/flowbite/2.3.0/flowbite.min.css" rel="stylesheet" />
- <style>
- body { font-family: sans-serif; background-color: #f9fafb; }
- .nav-btn { transition: all 0.2s; }
- .nav-btn.active { background-color: #3b82f6; color: white; }
- .loader { border: 4px solid #f3f3f3; border-top: 4px solid #3498db; border-radius: 50%; width: 30px; height: 30px; animation: spin 1s linear infinite; }
- @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
- .color-preview { width: 24px; height: 24px; border: 1px solid #ccc; border-radius: 4px; display: inline-block; vertical-align: middle; }
- .folder-tree ul { padding-left: 20px; }
- .folder-tree li { list-style-type: '📁'; padding-left: 5px; margin-top: 4px; }
- .folder-tree span { font-size: 0.8rem; color: #6b7280; }
- </style>
- </head>
- <body class="p-4">
- <div id="loading-overlay" class="fixed inset-0 bg-gray-500 bg-opacity-50 flex items-center justify-center z-50 hidden">
- <div class="loader"></div>
- </div>
- <div id="toast-success" class="fixed top-5 right-5 hidden items-center w-full max-w-xs p-4 mb-4 text-gray-500 bg-white rounded-lg shadow dark:text-gray-400 dark:bg-gray-800" role="alert">
- <div class="inline-flex items-center justify-center flex-shrink-0 w-8 h-8 text-green-500 bg-green-100 rounded-lg dark:bg-green-800 dark:text-green-200">
- <svg class="w-5 h-5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
- <path d="M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5Zm3.707 8.207-4 4a1 1 0 0 1-1.414 0l-2-2a1 1 0 0 1 1.414-1.414L9 10.586l3.293-3.293a1 1 0 0 1 1.414 1.414Z"/>
- </svg>
- </div>
- <div id="toast-success-msg" class="ms-3 text-sm font-normal">Item moved successfully.</div>
- </div>
- <div id="toast-danger" class="fixed top-5 right-5 hidden items-center w-full max-w-xs p-4 mb-4 text-gray-500 bg-white rounded-lg shadow dark:text-gray-400 dark:bg-gray-800" role="alert">
- <div class="inline-flex items-center justify-center flex-shrink-0 w-8 h-8 text-red-500 bg-red-100 rounded-lg dark:bg-red-800 dark:text-red-200">
- <svg class="w-5 h-5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
- <path d="M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5Zm3.707 11.793a1 1 0 1 1-1.414 1.414L10 11.414l-2.293 2.293a1 1 0 0 1-1.414-1.414L8.586 10 6.293 7.707a1 1 0 0 1 1.414-1.414L10 8.586l2.293-2.293a1 1 0 0 1 1.414 1.414L11.414 10l2.293 2.293Z"/>
- </svg>
- </div>
- <div id="toast-danger-msg" class="ms-3 text-sm font-normal">Item has been deleted.</div>
- </div>
- <nav class="flex space-x-2 border-b pb-2 mb-4">
- <button id="nav-stats" onclick="switchView('stats')" class="nav-btn px-4 py-2 rounded-lg font-semibold">Dashboard</button>
- <button id="nav-config" onclick="switchView('config')" class="nav-btn px-4 py-2 rounded-lg font-semibold">Color Config</button>
- <button id="nav-help" onclick="switchView('help')" class="nav-btn px-4 py-2 rounded-lg font-semibold">Help</button>
- </nav>
- <main>
- <div id="view-stats">
- <h2 class="text-2xl font-bold text-gray-800 mb-4">Indexing Dashboard</h2>
- <div id="capacity-warning" class="p-4 mb-4 text-sm text-yellow-800 rounded-lg bg-yellow-50 hidden" role="alert"></div>
- <div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
- <div class="p-4 bg-white rounded-lg shadow"><div class="text-sm font-medium text-gray-500">Total Files Indexed</div><div id="stats-file-count" class="text-3xl font-bold text-gray-900">...</div></div>
- <div class="p-4 bg-white rounded-lg shadow"><div class="text-sm font-medium text-gray-500">Parent Folders</div><div id="stats-folder-count" class="text-3xl font-bold text-gray-900">...</div></div>
- </div>
- <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
- <div class="p-4 bg-white rounded-lg shadow">
- <h3 class="font-semibold mb-2">Files by Type</h3>
- <div id="stats-mime-types" class="text-sm text-gray-700">Loading...</div>
- </div>
- <div class="p-4 bg-white rounded-lg shadow">
- <h3 class="font-semibold mb-2">Indexed Folder Summary</h3>
- <div id="stats-folder-tree" class="text-sm text-gray-700 folder-tree max-h-60 overflow-y-auto">Loading...</div>
- </div>
- </div>
- </div>
- <div id="view-config" class="hidden">
- <h2 class="text-2xl font-bold text-gray-800 mb-2">Folder Color Configuration</h2>
- <p class="text-sm text-gray-600 mb-4">Assign specific colors to parent folders. Unassigned folders will cycle through default colors.</p>
- <div id="color-config-list" class="space-y-2 mb-4"></div>
- <div class="flex items-center space-x-2">
- <input type="text" id="new-folder-name" placeholder="Folder Name" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5">
- <input type="text" id="new-folder-color" placeholder="#e0e7ff" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-auto p-2.5">
- <input type="color" id="color-picker" class="p-1 h-10 w-10 block bg-white border border-gray-300 cursor-pointer rounded-lg disabled:opacity-50 disabled:pointer-events-none">
- <button onclick="addConfigRow()" class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5">Add</button>
- </div>
- <div class="mt-6">
- <button onclick="saveColorConfig()" class="text-white bg-green-700 hover:bg-green-800 focus:ring-4 focus:ring-green-300 font-medium rounded-lg text-sm px-5 py-2.5">Save Configuration</button>
- </div>
- </div>
- <div id="view-help" class="hidden">
- <h2 class="text-2xl font-bold text-gray-800 mb-4">Help & Instructions</h2>
- <div class="space-y-4 text-gray-700">
- <div>
- <h3 class="font-semibold text-lg">How It Works</h3>
- <p>This tool scans specific Google Drive folders to create a master index in this spreadsheet. It's designed to handle a very large number of files without timing out by running in batches.</p>
- </div>
- <div>
- <h3 class="font-semibold text-lg">Which Folders Are Scanned?</h3>
- <p>The script is hardcoded to be as useful and safe as possible. It will automatically find and index files from <strong class="text-blue-600">all folders that have been shared with you</strong>. This avoids indexing your personal "My Drive" clutter and focuses on collaborative case folders. You do not need to specify a folder manually.</p>
- </div>
- <div>
- <h3 class="font-semibold text-lg">How to Use</h3>
- <ol class="list-decimal list-inside space-y-2 pl-4">
- <li><strong>Start Scan:</strong> Go to the <code class="bg-gray-200 p-1 rounded">🗂️ File Indexer > 🔍 Start/Update Index Scan</code> menu. This will begin the process. A small notification will appear. For very large folders, this can take many minutes.</li>
- <li><strong>View Progress:</strong> You can open this Dashboard at any time to see the latest stats. Refresh the dashboard by closing and reopening it.</li>
- <li><strong>Customize Colors:</strong> Use the "Color Config" tab to set specific colors for important parent folders for better visual organization.</li>
- <li><strong>Updating:</strong> Simply run the "Start/Update Index Scan" again. The script is smart and will only add new files it hasn't indexed before.</li>
- </ol>
- </div>
- </div>
- </div>
- </main>
- <script src="https://cdnjs.cloudflare.com/ajax/libs/flowbite/2.3.0/flowbite.min.js"></script>
- <script>
- const views = ['stats', 'config', 'help'];
- function showLoader() { document.getElementById('loading-overlay').classList.remove('hidden'); }
- function hideLoader() { document.getElementById('loading-overlay').classList.add('hidden'); }
- function showToast(type, message) {
- const toastId = \`toast-\${type}\`;
- const msgId = \`toast-\${type}-msg\`;
- document.getElementById(msgId).textContent = message;
- const toast = document.getElementById(toastId);
- toast.classList.remove('hidden');
- toast.classList.add('flex');
- setTimeout(() => {
- toast.classList.add('hidden');
- toast.classList.remove('flex');
- }, 5000);
- }
- function switchView(viewName) {
- views.forEach(v => {
- document.getElementById(\`view-\${v}\`).classList.add('hidden');
- document.getElementById(\`nav-\${v}\`).classList.remove('active');
- });
- document.getElementById(\`view-\${viewName}\`).classList.remove('hidden');
- document.getElementById(\`nav-\${viewName}\`).classList.add('active');
- if (viewName === 'config') loadColorConfig();
- if (viewName === 'stats') loadDashboard();
- }
- function loadDashboard() {
- showLoader();
- google.script.run
- .withSuccessHandler(stats => {
- hideLoader();
- if(stats.error) {
- showToast('danger', stats.error);
- return;
- }
- document.getElementById('stats-file-count').textContent = stats.fileCount;
- document.getElementById('stats-folder-count').textContent = stats.parentFolderCount;
- if (stats.capacityWarning) {
- const warningDiv = document.getElementById('capacity-warning');
- warningDiv.textContent = stats.capacityWarning;
- warningDiv.classList.remove('hidden');
- }
- const mimeDiv = document.getElementById('stats-mime-types');
- mimeDiv.innerHTML = '';
- if(Object.keys(stats.mimeTypeCounts).length > 0) {
- const list = document.createElement('ul');
- list.className = "list-disc list-inside";
- Object.entries(stats.mimeTypeCounts).forEach(([type, count]) => {
- const item = document.createElement('li');
- item.textContent = \`\${type}: \${count}\`;
- list.appendChild(item);
- });
- mimeDiv.appendChild(list);
- } else {
- mimeDiv.textContent = 'No files indexed yet.';
- }
- const treeDiv = document.getElementById('stats-folder-tree');
- treeDiv.innerHTML = '';
- if(stats.folderTree && stats.folderTree.length > 0) {
- const ul = document.createElement('ul');
- stats.folderTree.forEach(folder => {
- const li = document.createElement('li');
- li.innerHTML = \`<a href="\${folder.url}" target="_blank" class="text-blue-600 hover:underline">\${folder.name}</a> <span>(\${folder.count} files)</span>\`;
- ul.appendChild(li);
- });
- treeDiv.appendChild(ul);
- } else {
- treeDiv.textContent = 'No folders indexed yet.';
- }
- })
- .withFailureHandler(err => { hideLoader(); showToast('danger', err.message); })
- .getDashboardStats();
- }
- function loadColorConfig() {
- showLoader();
- google.script.run
- .withSuccessHandler(config => {
- hideLoader();
- if(config.error) {
- showToast('danger', config.error);
- return;
- }
- const listDiv = document.getElementById('color-config-list');
- listDiv.innerHTML = '';
- Object.entries(config).forEach(([folder, color]) => {
- createConfigRow(folder, color);
- });
- })
- .withFailureHandler(err => { hideLoader(); showToast('danger', err.message); })
- .getColorConfig();
- }
- function saveColorConfig() {
- showLoader();
- const config = {};
- document.querySelectorAll('.config-row').forEach(row => {
- const folder = row.querySelector('.folder-name-input').value;
- const color = row.querySelector('.color-hex-input').value;
- if (folder && color) {
- config[folder] = color;
- }
- });
- google.script.run
- .withSuccessHandler(response => {
- hideLoader();
- if(response.error) showToast('danger', response.error);
- else showToast('success', response.success);
- })
- .withFailureHandler(err => { hideLoader(); showToast('danger', err.message); })
- .saveColorConfig(config);
- }
- function addConfigRow() {
- const folder = document.getElementById('new-folder-name').value;
- const color = document.getElementById('new-folder-color').value;
- if (!folder || !color) {
- showToast('danger', 'Please enter both a folder name and a hex color.');
- return;
- }
- createConfigRow(folder, color);
- document.getElementById('new-folder-name').value = '';
- document.getElementById('new-folder-color').value = '';
- }
- function createConfigRow(folder, color) {
- const listDiv = document.getElementById('color-config-list');
- const row = document.createElement('div');
- row.className = 'config-row flex items-center space-x-2';
- row.innerHTML = \`
- <input type="text" value="\${folder}" class="folder-name-input bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg block w-full p-2.5">
- <div class="color-preview" style="background-color: \${color};"></div>
- <input type="text" value="\${color}" class="color-hex-input bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg w-auto p-2.5">
- <button onclick="this.parentElement.remove()" class="text-white bg-red-600 hover:bg-red-700 font-medium rounded-lg text-sm px-3 py-2.5">X</button>
- \`;
- listDiv.appendChild(row);
- }
- document.addEventListener('DOMContentLoaded', () => {
- switchView('stats');
- const colorPicker = document.getElementById('color-picker');
- const colorHexInput = document.getElementById('new-folder-color');
- colorPicker.addEventListener('input', (e) => {
- colorHexInput.value = e.target.value;
- });
- colorHexInput.addEventListener('input', (e) => {
- const preview = document.createElement('div');
- preview.style.backgroundColor = e.target.value;
- if (preview.style.backgroundColor) { // a valid color
- colorPicker.value = e.target.value;
- }
- });
- });
- </script>
- </body>
- </html>
- `;
- }
Advertisement
Add Comment
Please, Sign In to add comment