jaybee126

Untitled

Aug 8th, 2025
20
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 31.98 KB | None | 0 0
  1. 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.
  2. 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.
  3. 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.
  4. Instructions for Installation
  5. * Create a new Google Sheet. Name it something like "Case File Index".
  6. * Go to Extensions > Apps Script.
  7. * Delete any code in the Code.gs file.
  8. * Copy the entire code block below and paste it into the Code.gs editor.
  9. * Click the Save project icon (looks like a floppy disk).
  10. * Go back to your Google Sheet and refresh the page. A new menu named 🗂️ File Indexer should appear.
  11. * 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.
  12. The Complete Code.gs Script
  13. // Copyright 2025
  14. // Developed for a legal assistant to index large Google Drive folders.
  15. // This script is designed to be robust, user-friendly, and handle large file counts
  16. // by using a resumable, batched process to avoid timeouts.
  17.  
  18. // --- CONFIGURATION & CONSTANTS --- //
  19. const INDEX_SHEET_NAME = 'File Index';
  20. const CONFIG_SHEET_NAME = 'Configuration';
  21. const HEADERS = ['Parent Folder', 'File Name', 'Date Created', 'File Type', 'Link'];
  22. const DEFAULT_COLORS = [
  23. '#e0e7ff', '#fee2e2', '#d1fae5', '#fef3c7', '#f3e8ff',
  24. '#dbeafe', '#ffe4e6', '#cce7e0', '#fef9c3', '#ede9fe',
  25. '#c7d2fe', '#fecdd3', '#bbf7d0', '#fde68a', '#ddd6fe'
  26. ];
  27. const MAX_RUNTIME_MS = 5 * 60 * 1000; // 5 minutes to be safe from 6-min timeout.
  28. const SCRIPT_LOCK_TIMEOUT_MS = 30 * 1000; // 30 seconds.
  29. const SHEET_CAPACITY_THRESHOLD = 0.95; // Warn user at 95% sheet capacity.
  30.  
  31. // --- SPREADSHEET UI & INITIALIZATION --- //
  32.  
  33. /**
  34. * Creates a custom menu in the spreadsheet when the file is opened.
  35. * @param {object} e The event object.
  36. */
  37. function onOpen(e) {
  38. SpreadsheetApp.getUi()
  39. .createMenu('🗂️ File Indexer')
  40. .addItem('🚀 Open Dashboard', 'showIndexerModal')
  41. .addSeparator()
  42. .addItem('🔍 Start/Update Index Scan', 'startIndexingProcess')
  43. .addToUi();
  44.  
  45. // Ensure required sheets exist
  46. const ss = SpreadsheetApp.getActiveSpreadsheet();
  47. if (!ss.getSheetByName(INDEX_SHEET_NAME)) {
  48. const sheet = ss.insertSheet(INDEX_SHEET_NAME);
  49. sheet.appendRow(HEADERS).setFrozenRows(1);
  50. sheet.getRange("A1:E1").setFontWeight('bold');
  51. }
  52. if (!ss.getSheetByName(CONFIG_SHEET_NAME)) {
  53. const configSheet = ss.insertSheet(CONFIG_SHEET_NAME);
  54. const protection = configSheet.protect().setDescription('Configuration sheet protected by script');
  55. const me = Session.getEffectiveUser();
  56. protection.addEditor(me);
  57. protection.removeEditors(protection.getEditors());
  58. if (protection.canDomainEdit()) {
  59. protection.setDomainEdit(false);
  60. }
  61. }
  62. }
  63.  
  64. /**
  65. * Displays the main UI modal dialog.
  66. */
  67. function showIndexerModal() {
  68. const htmlOutput = HtmlService.createHtmlOutput(getModalHtml())
  69. .setWidth(900)
  70. .setHeight(650);
  71. SpreadsheetApp.getUi().showModalDialog(htmlOutput, '🗂️ File Indexer Dashboard');
  72. }
  73.  
  74. // --- BACKEND FUNCTIONS CALLED FROM UI --- //
  75.  
  76. /**
  77. * Fetches statistics for the dashboard landing page.
  78. * @returns {object} An object containing dashboard statistics.
  79. */
  80. function getDashboardStats() {
  81. try {
  82. const ss = SpreadsheetApp.getActiveSpreadsheet();
  83. const indexSheet = ss.getSheetByName(INDEX_SHEET_NAME);
  84. const configSheet = ss.getSheetByName(CONFIG_SHEET_NAME);
  85. const dataRange = indexSheet.getDataRange();
  86. const values = dataRange.getValues();
  87.  
  88. const fileCount = Math.max(0, values.length - 1);
  89. const parentFolders = new Set(values.slice(1).map(row => row[0]));
  90.  
  91. const mimeTypeCounts = values.slice(1).reduce((acc, row) => {
  92. const type = row[3] || 'Unknown';
  93. acc[type] = (acc[type] || 0) + 1;
  94. return acc;
  95. }, {});
  96.  
  97. const maxRows = ss.getMaxRows();
  98. const currentRowCount = indexSheet.getLastRow();
  99. const capacity = currentRowCount / maxRows;
  100. let capacityWarning = null;
  101. if (capacity > SHEET_CAPACITY_THRESHOLD) {
  102. capacityWarning = `Warning: Sheet is at ${Math.round(capacity * 100)}% capacity (${currentRowCount} of ${maxRows} rows).`;
  103. }
  104.  
  105. const folderTree = getFolderTree();
  106.  
  107. return {
  108. fileCount: fileCount,
  109. parentFolderCount: parentFolders.size,
  110. mimeTypeCounts: mimeTypeCounts,
  111. capacityWarning: capacityWarning,
  112. folderTree: folderTree,
  113. };
  114. } catch (e) {
  115. return { error: `Could not load stats: ${e.message}` };
  116. }
  117. }
  118.  
  119. /**
  120. * Retrieves the color configuration from the 'Configuration' sheet.
  121. * @returns {object} Key-value pairs of folder names and hex colors.
  122. */
  123. function getColorConfig() {
  124. try {
  125. const configSheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(CONFIG_SHEET_NAME);
  126. if (!configSheet) return {};
  127. const data = configSheet.getDataRange().getValues();
  128. return data.reduce((acc, row) => {
  129. if (row[0] && row[1]) acc[row[0]] = row[1];
  130. return acc;
  131. }, {});
  132. } catch (e) {
  133. return { error: `Could not load color config: ${e.message}` };
  134. }
  135. }
  136.  
  137. /**
  138. * Saves the color configuration to the 'Configuration' sheet.
  139. * @param {object} config The configuration object to save.
  140. * @returns {object} A success or error message.
  141. */
  142. function saveColorConfig(config) {
  143. try {
  144. const configSheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(CONFIG_SHEET_NAME);
  145. configSheet.clearContents();
  146. const rows = Object.entries(config);
  147. if (rows.length > 0) {
  148. configSheet.getRange(1, 1, rows.length, 2).setValues(rows);
  149. }
  150. return { success: 'Color configuration saved successfully!' };
  151. } catch (e) {
  152. return { error: `Failed to save configuration: ${e.message}` };
  153. }
  154. }
  155.  
  156. /**
  157. * Builds a hierarchical tree of indexed folders and their file counts.
  158. * This is a simplified version for the UI and reads from the already indexed sheet.
  159. * @returns {object} A nested object representing the folder tree.
  160. */
  161. function getFolderTree() {
  162. const ss = SpreadsheetApp.getActiveSpreadsheet();
  163. const indexSheet = ss.getSheetByName(INDEX_SHEET_NAME);
  164. if (indexSheet.getLastRow() <= 1) return {};
  165.  
  166. const values = indexSheet.getRange(2, 1, indexSheet.getLastRow() - 1, 5).getValues();
  167. const folderData = {}; // { folderId: { name, url, count } }
  168. const fileData = {}; // { fileId: { parentFolderId } }
  169.  
  170. values.forEach(row => {
  171. try {
  172. const parentFolderName = row[0];
  173. const fileUrl = row[4];
  174. if (!parentFolderName || !fileUrl) return;
  175.  
  176. const fileIdMatch = fileUrl.match(/id=([^&]+)/);
  177. if (!fileIdMatch) return;
  178. const fileId = fileIdMatch[1];
  179.  
  180. const file = DriveApp.getFileById(fileId);
  181. const parents = file.getParents();
  182. if (parents.hasNext()) {
  183. const parentFolder = parents.next();
  184. const parentId = parentFolder.getId();
  185.  
  186. if (!folderData[parentId]) {
  187. folderData[parentId] = {
  188. name: parentFolder.getName(),
  189. url: parentFolder.getUrl(),
  190. count: 0
  191. };
  192. }
  193. folderData[parentId].count++;
  194. }
  195. } catch (e) {
  196. // Ignore files that may have been deleted or are inaccessible
  197. }
  198. });
  199.  
  200. return Object.values(folderData).sort((a,b) => a.name.localeCompare(b.name));
  201. }
  202.  
  203.  
  204. // --- CORE INDEXING LOGIC (RESUMABLE/BATCHED) --- //
  205.  
  206. /**
  207. * Initiates the indexing process.
  208. * This function sets up the state for a resumable, triggered execution.
  209. */
  210. function startIndexingProcess() {
  211. const lock = LockService.getScriptLock();
  212. if (!lock.tryLock(SCRIPT_LOCK_TIMEOUT_MS)) {
  213. SpreadsheetApp.getUi().alert('An indexing process is already running. Please wait for it to complete.');
  214. return;
  215. }
  216.  
  217. try {
  218. // Clear any previous triggers to start fresh
  219. const allTriggers = ScriptApp.getProjectTriggers();
  220. for (const trigger of allTriggers) {
  221. if (trigger.getHandlerFunction() === 'continueIndexingProcess') {
  222. ScriptApp.deleteTrigger(trigger);
  223. }
  224. }
  225.  
  226. const properties = PropertiesService.getScriptProperties();
  227.  
  228. // Get all existing file IDs from the index to avoid duplicates
  229. const indexSheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(INDEX_SHEET_NAME);
  230. const existingFileIds = new Set();
  231. const idColumnValues = indexSheet.getRange("E2:E").getValues();
  232. idColumnValues.forEach(row => {
  233. if(row[0]) {
  234. const match = row[0].match(/id=([^&]+)/);
  235. if (match) existingFileIds.add(match[1]);
  236. }
  237. });
  238.  
  239. // The user's desired search query: find all folders that are shared with them.
  240. // This intelligently targets collaborative folders.
  241. const searchQuery = 'mimeType="application/vnd.google-apps.folder" and sharedWithMe';
  242. const folders = DriveApp.searchFolders(searchQuery);
  243.  
  244. const folderQueue = [];
  245. while(folders.hasNext()){
  246. folderQueue.push(folders.next().getId());
  247. }
  248.  
  249. if (folderQueue.length === 0) {
  250. SpreadsheetApp.getUi().alert('No shared folders found matching the criteria. Nothing to index.');
  251. return;
  252. }
  253.  
  254. // Store initial state for the batch processor
  255. properties.setProperties({
  256. 'folderQueue': JSON.stringify(folderQueue),
  257. 'currentFolderIndex': '0',
  258. 'existingFileIds': JSON.stringify(Array.from(existingFileIds)),
  259. 'colorConfigCache': JSON.stringify(getColorConfig()),
  260. 'lastAppliedColorIndex': '0'
  261. });
  262.  
  263. // Start the first batch
  264. ScriptApp.newTrigger('continueIndexingProcess').timeBased().after(1000).create();
  265. SpreadsheetApp.getUi().showToast('🚀 Indexing scan has started! This may take several minutes. You can close this window.', 'Process Started', 10);
  266.  
  267. } catch(e) {
  268. SpreadsheetApp.getUi().alert(`An error occurred while starting the scan: ${e.message}`);
  269. } finally {
  270. lock.releaseLock();
  271. }
  272. }
  273.  
  274. /**
  275. * Processes a batch of files. This function is called by a trigger
  276. * and re-schedules itself until all folders are processed.
  277. */
  278. function continueIndexingProcess() {
  279. const lock = LockService.getScriptLock();
  280. if (!lock.tryLock(SCRIPT_LOCK_TIMEOUT_MS)) {
  281. console.warn('Skipping execution because a previous one is still running.');
  282. return;
  283. }
  284.  
  285. const startTime = Date.now();
  286. const properties = PropertiesService.getScriptProperties();
  287. const state = properties.getProperties();
  288.  
  289. const folderQueue = JSON.parse(state.folderQueue || '[]');
  290. let currentFolderIndex = parseInt(state.currentFolderIndex || '0');
  291. const existingFileIds = new Set(JSON.parse(state.existingFileIds || '[]'));
  292.  
  293. try {
  294. if (currentFolderIndex >= folderQueue.length) {
  295. cleanupTriggersAndFinish();
  296. return;
  297. }
  298.  
  299. const newRows = [];
  300. const ss = SpreadsheetApp.getActiveSpreadsheet();
  301. const indexSheet = ss.getSheetByName(INDEX_SHEET_NAME);
  302.  
  303. while (currentFolderIndex < folderQueue.length) {
  304. const folderId = folderQueue[currentFolderIndex];
  305. const folder = DriveApp.getFolderById(folderId);
  306.  
  307. // Recursive function to process files within this folder and its subfolders
  308. processFolderRecursively(folder, folder, newRows, existingFileIds, startTime);
  309.  
  310. currentFolderIndex++;
  311. properties.setProperty('currentFolderIndex', currentFolderIndex.toString());
  312.  
  313. // If we are running out of time, write what we have and reschedule
  314. if (Date.now() - startTime > MAX_RUNTIME_MS) {
  315. if (newRows.length > 0) {
  316. writeAndColorRows(indexSheet, newRows, state);
  317. }
  318. reschedule();
  319. lock.releaseLock();
  320. return;
  321. }
  322. }
  323.  
  324. // Write any remaining rows from the last folder
  325. if (newRows.length > 0) {
  326. writeAndColorRows(indexSheet, newRows, state);
  327. }
  328.  
  329. cleanupTriggersAndFinish();
  330.  
  331. } catch (e) {
  332. console.error(`Error during indexing: ${e.message} Stack: ${e.stack}`);
  333. cleanupTriggersAndFinish(true); // Finish with error
  334. } finally {
  335. lock.releaseLock();
  336. }
  337. }
  338.  
  339. /**
  340. * Helper function to recursively traverse folders.
  341. */
  342. function processFolderRecursively(rootFolder, currentFolder, newRows, existingFileIds, startTime) {
  343. if (Date.now() - startTime > MAX_RUNTIME_MS) return;
  344.  
  345. const files = currentFolder.getFiles();
  346. while (files.hasNext()) {
  347. if (Date.now() - startTime > MAX_RUNTIME_MS) return;
  348. const file = files.next();
  349. if (!existingFileIds.has(file.getId())) {
  350. newRows.push([
  351. rootFolder.getName(),
  352. file.getName(),
  353. file.getDateCreated(),
  354. file.getMimeType(),
  355. file.getUrl()
  356. ]);
  357. existingFileIds.add(file.getId()); // Add to set to avoid duplicates in the same run
  358. }
  359. }
  360.  
  361. const subFolders = currentFolder.getFolders();
  362. while (subFolders.hasNext()) {
  363. if (Date.now() - startTime > MAX_RUNTIME_MS) return;
  364. processFolderRecursively(rootFolder, subFolders.next(), newRows, existingFileIds, startTime);
  365. }
  366. }
  367.  
  368. /**
  369. * Writes new rows to the sheet and applies color coding.
  370. */
  371. function writeAndColorRows(sheet, rows, state) {
  372. const startRow = sheet.getLastRow() + 1;
  373. sheet.getRange(startRow, 1, rows.length, HEADERS.length).setValues(rows);
  374.  
  375. // Apply color coding
  376. const colorConfig = JSON.parse(state.colorConfigCache || '{}');
  377. const parentFolderColorMap = {};
  378. let lastAppliedColorIndex = parseInt(state.lastAppliedColorIndex || '0');
  379.  
  380. const range = sheet.getRange(startRow, 1, rows.length, HEADERS.length);
  381. const backgroundColors = range.getBackgrounds();
  382. const folderNames = range.getValues().map(row => row[0]);
  383.  
  384. for (let i = 0; i < folderNames.length; i++) {
  385. const folderName = folderNames[i];
  386. if (!parentFolderColorMap[folderName]) {
  387. if (colorConfig[folderName]) {
  388. parentFolderColorMap[folderName] = colorConfig[folderName];
  389. } else {
  390. parentFolderColorMap[folderName] = DEFAULT_COLORS[lastAppliedColorIndex % DEFAULT_COLORS.length];
  391. lastAppliedColorIndex++;
  392. }
  393. }
  394. backgroundColors[i] = Array(HEADERS.length).fill(parentFolderColorMap[folderName]);
  395. }
  396.  
  397. range.setBackgrounds(backgroundColors);
  398. PropertiesService.getScriptProperties().setProperty('lastAppliedColorIndex', lastAppliedColorIndex.toString());
  399. }
  400.  
  401.  
  402. /**
  403. * Reschedules the 'continueIndexingProcess' to run again.
  404. */
  405. function reschedule() {
  406. ScriptApp.newTrigger('continueIndexingProcess').timeBased().after(1000).create();
  407. console.log('Rescheduling next batch...');
  408. }
  409.  
  410. /**
  411. * Cleans up triggers and properties, then notifies the user.
  412. */
  413. function cleanupTriggersAndFinish(isError = false) {
  414. const allTriggers = ScriptApp.getProjectTriggers();
  415. for (const trigger of allTriggers) {
  416. if (trigger.getHandlerFunction() === 'continueIndexingProcess') {
  417. ScriptApp.deleteTrigger(trigger);
  418. }
  419. }
  420. PropertiesService.getScriptProperties().deleteAllProperties();
  421.  
  422. const message = isError ? '❌ Indexing stopped due to an error. Please check logs.' : '✅ Indexing complete! All files have been processed.';
  423. const title = isError ? 'Error' : 'Success';
  424. SpreadsheetApp.getActiveSpreadsheet().toast(message, title, 10);
  425. console.log(message);
  426. }
  427.  
  428.  
  429. // --- HTML & CLIENT-SIDE JAVASCRIPT FOR MODAL --- //
  430.  
  431. function getModalHtml() {
  432. return `
  433. <!DOCTYPE html>
  434. <html>
  435. <head>
  436. <base target="_top">
  437. <link href="https://cdnjs.cloudflare.com/ajax/libs/flowbite/2.3.0/flowbite.min.css" rel="stylesheet" />
  438. <style>
  439. body { font-family: sans-serif; background-color: #f9fafb; }
  440. .nav-btn { transition: all 0.2s; }
  441. .nav-btn.active { background-color: #3b82f6; color: white; }
  442. .loader { border: 4px solid #f3f3f3; border-top: 4px solid #3498db; border-radius: 50%; width: 30px; height: 30px; animation: spin 1s linear infinite; }
  443. @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
  444. .color-preview { width: 24px; height: 24px; border: 1px solid #ccc; border-radius: 4px; display: inline-block; vertical-align: middle; }
  445. .folder-tree ul { padding-left: 20px; }
  446. .folder-tree li { list-style-type: '📁'; padding-left: 5px; margin-top: 4px; }
  447. .folder-tree span { font-size: 0.8rem; color: #6b7280; }
  448. </style>
  449. </head>
  450. <body class="p-4">
  451. <div id="loading-overlay" class="fixed inset-0 bg-gray-500 bg-opacity-50 flex items-center justify-center z-50 hidden">
  452. <div class="loader"></div>
  453. </div>
  454. <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">
  455. <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">
  456. <svg class="w-5 h-5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
  457. <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"/>
  458. </svg>
  459. </div>
  460. <div id="toast-success-msg" class="ms-3 text-sm font-normal">Item moved successfully.</div>
  461. </div>
  462. <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">
  463. <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">
  464. <svg class="w-5 h-5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
  465. <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"/>
  466. </svg>
  467. </div>
  468. <div id="toast-danger-msg" class="ms-3 text-sm font-normal">Item has been deleted.</div>
  469. </div>
  470.  
  471. <nav class="flex space-x-2 border-b pb-2 mb-4">
  472. <button id="nav-stats" onclick="switchView('stats')" class="nav-btn px-4 py-2 rounded-lg font-semibold">Dashboard</button>
  473. <button id="nav-config" onclick="switchView('config')" class="nav-btn px-4 py-2 rounded-lg font-semibold">Color Config</button>
  474. <button id="nav-help" onclick="switchView('help')" class="nav-btn px-4 py-2 rounded-lg font-semibold">Help</button>
  475. </nav>
  476.  
  477. <main>
  478. <div id="view-stats">
  479. <h2 class="text-2xl font-bold text-gray-800 mb-4">Indexing Dashboard</h2>
  480. <div id="capacity-warning" class="p-4 mb-4 text-sm text-yellow-800 rounded-lg bg-yellow-50 hidden" role="alert"></div>
  481. <div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
  482. <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>
  483. <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>
  484. </div>
  485. <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
  486. <div class="p-4 bg-white rounded-lg shadow">
  487. <h3 class="font-semibold mb-2">Files by Type</h3>
  488. <div id="stats-mime-types" class="text-sm text-gray-700">Loading...</div>
  489. </div>
  490. <div class="p-4 bg-white rounded-lg shadow">
  491. <h3 class="font-semibold mb-2">Indexed Folder Summary</h3>
  492. <div id="stats-folder-tree" class="text-sm text-gray-700 folder-tree max-h-60 overflow-y-auto">Loading...</div>
  493. </div>
  494. </div>
  495. </div>
  496.  
  497. <div id="view-config" class="hidden">
  498. <h2 class="text-2xl font-bold text-gray-800 mb-2">Folder Color Configuration</h2>
  499. <p class="text-sm text-gray-600 mb-4">Assign specific colors to parent folders. Unassigned folders will cycle through default colors.</p>
  500. <div id="color-config-list" class="space-y-2 mb-4"></div>
  501. <div class="flex items-center space-x-2">
  502. <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">
  503. <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">
  504. <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">
  505. <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>
  506. </div>
  507. <div class="mt-6">
  508. <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>
  509. </div>
  510. </div>
  511.  
  512. <div id="view-help" class="hidden">
  513. <h2 class="text-2xl font-bold text-gray-800 mb-4">Help & Instructions</h2>
  514. <div class="space-y-4 text-gray-700">
  515. <div>
  516. <h3 class="font-semibold text-lg">How It Works</h3>
  517. <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>
  518. </div>
  519. <div>
  520. <h3 class="font-semibold text-lg">Which Folders Are Scanned?</h3>
  521. <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>
  522. </div>
  523. <div>
  524. <h3 class="font-semibold text-lg">How to Use</h3>
  525. <ol class="list-decimal list-inside space-y-2 pl-4">
  526. <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>
  527. <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>
  528. <li><strong>Customize Colors:</strong> Use the "Color Config" tab to set specific colors for important parent folders for better visual organization.</li>
  529. <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>
  530. </ol>
  531. </div>
  532. </div>
  533. </div>
  534. </main>
  535.  
  536. <script src="https://cdnjs.cloudflare.com/ajax/libs/flowbite/2.3.0/flowbite.min.js"></script>
  537. <script>
  538. const views = ['stats', 'config', 'help'];
  539.  
  540. function showLoader() { document.getElementById('loading-overlay').classList.remove('hidden'); }
  541. function hideLoader() { document.getElementById('loading-overlay').classList.add('hidden'); }
  542.  
  543. function showToast(type, message) {
  544. const toastId = \`toast-\${type}\`;
  545. const msgId = \`toast-\${type}-msg\`;
  546. document.getElementById(msgId).textContent = message;
  547. const toast = document.getElementById(toastId);
  548. toast.classList.remove('hidden');
  549. toast.classList.add('flex');
  550. setTimeout(() => {
  551. toast.classList.add('hidden');
  552. toast.classList.remove('flex');
  553. }, 5000);
  554. }
  555.  
  556. function switchView(viewName) {
  557. views.forEach(v => {
  558. document.getElementById(\`view-\${v}\`).classList.add('hidden');
  559. document.getElementById(\`nav-\${v}\`).classList.remove('active');
  560. });
  561. document.getElementById(\`view-\${viewName}\`).classList.remove('hidden');
  562. document.getElementById(\`nav-\${viewName}\`).classList.add('active');
  563.  
  564. if (viewName === 'config') loadColorConfig();
  565. if (viewName === 'stats') loadDashboard();
  566. }
  567.  
  568. function loadDashboard() {
  569. showLoader();
  570. google.script.run
  571. .withSuccessHandler(stats => {
  572. hideLoader();
  573. if(stats.error) {
  574. showToast('danger', stats.error);
  575. return;
  576. }
  577. document.getElementById('stats-file-count').textContent = stats.fileCount;
  578. document.getElementById('stats-folder-count').textContent = stats.parentFolderCount;
  579.  
  580. if (stats.capacityWarning) {
  581. const warningDiv = document.getElementById('capacity-warning');
  582. warningDiv.textContent = stats.capacityWarning;
  583. warningDiv.classList.remove('hidden');
  584. }
  585.  
  586. const mimeDiv = document.getElementById('stats-mime-types');
  587. mimeDiv.innerHTML = '';
  588. if(Object.keys(stats.mimeTypeCounts).length > 0) {
  589. const list = document.createElement('ul');
  590. list.className = "list-disc list-inside";
  591. Object.entries(stats.mimeTypeCounts).forEach(([type, count]) => {
  592. const item = document.createElement('li');
  593. item.textContent = \`\${type}: \${count}\`;
  594. list.appendChild(item);
  595. });
  596. mimeDiv.appendChild(list);
  597. } else {
  598. mimeDiv.textContent = 'No files indexed yet.';
  599. }
  600.  
  601. const treeDiv = document.getElementById('stats-folder-tree');
  602. treeDiv.innerHTML = '';
  603. if(stats.folderTree && stats.folderTree.length > 0) {
  604. const ul = document.createElement('ul');
  605. stats.folderTree.forEach(folder => {
  606. const li = document.createElement('li');
  607. li.innerHTML = \`<a href="\${folder.url}" target="_blank" class="text-blue-600 hover:underline">\${folder.name}</a> <span>(\${folder.count} files)</span>\`;
  608. ul.appendChild(li);
  609. });
  610. treeDiv.appendChild(ul);
  611. } else {
  612. treeDiv.textContent = 'No folders indexed yet.';
  613. }
  614. })
  615. .withFailureHandler(err => { hideLoader(); showToast('danger', err.message); })
  616. .getDashboardStats();
  617. }
  618.  
  619. function loadColorConfig() {
  620. showLoader();
  621. google.script.run
  622. .withSuccessHandler(config => {
  623. hideLoader();
  624. if(config.error) {
  625. showToast('danger', config.error);
  626. return;
  627. }
  628. const listDiv = document.getElementById('color-config-list');
  629. listDiv.innerHTML = '';
  630. Object.entries(config).forEach(([folder, color]) => {
  631. createConfigRow(folder, color);
  632. });
  633. })
  634. .withFailureHandler(err => { hideLoader(); showToast('danger', err.message); })
  635. .getColorConfig();
  636. }
  637.  
  638. function saveColorConfig() {
  639. showLoader();
  640. const config = {};
  641. document.querySelectorAll('.config-row').forEach(row => {
  642. const folder = row.querySelector('.folder-name-input').value;
  643. const color = row.querySelector('.color-hex-input').value;
  644. if (folder && color) {
  645. config[folder] = color;
  646. }
  647. });
  648. google.script.run
  649. .withSuccessHandler(response => {
  650. hideLoader();
  651. if(response.error) showToast('danger', response.error);
  652. else showToast('success', response.success);
  653. })
  654. .withFailureHandler(err => { hideLoader(); showToast('danger', err.message); })
  655. .saveColorConfig(config);
  656. }
  657.  
  658. function addConfigRow() {
  659. const folder = document.getElementById('new-folder-name').value;
  660. const color = document.getElementById('new-folder-color').value;
  661. if (!folder || !color) {
  662. showToast('danger', 'Please enter both a folder name and a hex color.');
  663. return;
  664. }
  665. createConfigRow(folder, color);
  666. document.getElementById('new-folder-name').value = '';
  667. document.getElementById('new-folder-color').value = '';
  668. }
  669.  
  670. function createConfigRow(folder, color) {
  671. const listDiv = document.getElementById('color-config-list');
  672. const row = document.createElement('div');
  673. row.className = 'config-row flex items-center space-x-2';
  674. row.innerHTML = \`
  675. <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">
  676. <div class="color-preview" style="background-color: \${color};"></div>
  677. <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">
  678. <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>
  679. \`;
  680. listDiv.appendChild(row);
  681. }
  682.  
  683. document.addEventListener('DOMContentLoaded', () => {
  684. switchView('stats');
  685. const colorPicker = document.getElementById('color-picker');
  686. const colorHexInput = document.getElementById('new-folder-color');
  687. colorPicker.addEventListener('input', (e) => {
  688. colorHexInput.value = e.target.value;
  689. });
  690. colorHexInput.addEventListener('input', (e) => {
  691. const preview = document.createElement('div');
  692. preview.style.backgroundColor = e.target.value;
  693. if (preview.style.backgroundColor) { // a valid color
  694. colorPicker.value = e.target.value;
  695. }
  696. });
  697. });
  698. </script>
  699. </body>
  700. </html>
  701. `;
  702. }
  703.  
Advertisement
Add Comment
Please, Sign In to add comment