diff --git a/electron-builder.yml b/electron-builder.yml index 95647edb..5cbe2e42 100644 --- a/electron-builder.yml +++ b/electron-builder.yml @@ -5,6 +5,7 @@ extraResources: [ { from: 'log_creds.txt', to: '../creds/log_creds.txt' }, { from: 'translations', to: '../translations' }, + { from: 'translations', to: '../translations' }, ] mac: type: distribution diff --git a/main.ts b/main.ts index 956b8207..792e6dd7 100644 --- a/main.ts +++ b/main.ts @@ -4,7 +4,7 @@ import { app, BrowserWindow, BrowserWindowConstructorOptions, - protocol + protocol, } from 'electron'; import Store from 'electron-store'; import { autoUpdater } from 'electron-updater'; diff --git a/main/getPrintTemplates.ts b/main/getPrintTemplates.ts new file mode 100644 index 00000000..d0aa48df --- /dev/null +++ b/main/getPrintTemplates.ts @@ -0,0 +1,41 @@ +import fs from 'fs/promises'; +import path from 'path'; +import { TemplateFile } from 'utils/types'; + +export async function getTemplates() { + const paths = await getPrintTemplatePaths(); + if (!paths) { + return []; + } + + const templates: TemplateFile[] = []; + for (const file of paths.files) { + const filePath = path.join(paths.root, file); + const template = await fs.readFile(filePath, 'utf-8'); + const { mtime } = await fs.stat(filePath); + templates.push({ template, file, modified: mtime.toISOString() }); + } + + return templates; +} + +async function getPrintTemplatePaths(): Promise<{ + files: string[]; + root: string; +} | null> { + let root = path.join(process.resourcesPath, `../templates`); + + try { + const files = await fs.readdir(root); + return { files, root }; + } catch { + root = path.join(__dirname, `../templates`); + } + + try { + const files = await fs.readdir(root); + return { files, root }; + } catch { + return null; + } +} diff --git a/main/registerIpcMainActionListeners.ts b/main/registerIpcMainActionListeners.ts index 17561f93..0a41cb74 100644 --- a/main/registerIpcMainActionListeners.ts +++ b/main/registerIpcMainActionListeners.ts @@ -10,6 +10,7 @@ import { DatabaseMethod } from '../utils/db/types'; import { IPC_ACTIONS } from '../utils/messages'; import { getUrlAndTokenString, sendError } from './contactMothership'; import { getLanguageMap } from './getLanguageMap'; +import { getTemplates } from './getPrintTemplates'; import { getConfigFilesWithModified, getErrorHandledReponse, @@ -117,7 +118,7 @@ export default function registerIpcMainActionListeners(main: Main) { ); ipcMain.handle(IPC_ACTIONS.GET_CREDS, async (event) => { - return await getUrlAndTokenString(); + return getUrlAndTokenString(); }); ipcMain.handle(IPC_ACTIONS.DELETE_FILE, async (_, filePath) => { @@ -137,6 +138,10 @@ export default function registerIpcMainActionListeners(main: Main) { }; }); + ipcMain.handle(IPC_ACTIONS.GET_TEMPLATES, async () => { + return getTemplates(); + }); + /** * Database Related Actions */ diff --git a/src/renderer.ts b/src/renderer.ts index 60e090ad..cd5ed510 100644 --- a/src/renderer.ts +++ b/src/renderer.ts @@ -111,6 +111,8 @@ function setOnWindow(isDevelopment: boolean) { window.fyo = fyo; // @ts-ignore window.DateTime = DateTime; + // @ts-ignore + window.ipcRenderer = ipcRenderer; } function getPlatformName(platform: string) { diff --git a/src/utils/initialization.ts b/src/utils/initialization.ts index c5184de7..d8352363 100644 --- a/src/utils/initialization.ts +++ b/src/utils/initialization.ts @@ -1,6 +1,5 @@ import { Fyo } from 'fyo'; import { ConfigFile, ConfigKeys } from 'fyo/core/types'; -import { Doc } from 'fyo/model/doc'; import { getRegionalModels, models } from 'models/index'; import { ModelNameEnum } from 'models/types'; import { TargetField } from 'schemas/types'; @@ -9,6 +8,7 @@ import { getRandomString, getValueMapFromList, } from 'utils/index'; +import { updatePrintTemplates } from './printTemplates'; export async function initializeInstance( dbPath: string, @@ -34,6 +34,7 @@ export async function initializeInstance( await setInstanceId(fyo); await setOpenCount(fyo); await setCurrencySymbols(fyo); + await updatePrintTemplates(fyo); } async function closeDbIfConnected(fyo: Fyo) { diff --git a/src/utils/ipcCalls.ts b/src/utils/ipcCalls.ts index 3049c82d..1f1d50ae 100644 --- a/src/utils/ipcCalls.ts +++ b/src/utils/ipcCalls.ts @@ -6,7 +6,7 @@ import { t } from 'fyo'; import { BaseError } from 'fyo/utils/errors'; import { BackendResponse } from 'utils/ipc/types'; import { IPC_ACTIONS, IPC_MESSAGES } from 'utils/messages'; -import { SelectFileOptions, SelectFileReturn } from 'utils/types'; +import { SelectFileOptions, SelectFileReturn, TemplateFile } from 'utils/types'; import { setLanguageMap } from './language'; import { showMessageDialog, showToast } from './ui'; @@ -14,6 +14,10 @@ export function reloadWindow() { return ipcRenderer.send(IPC_MESSAGES.RELOAD_MAIN_WINDOW); } +export async function getTemplates(): Promise { + return await ipcRenderer.invoke(IPC_ACTIONS.GET_TEMPLATES); +} + export async function selectFile( options: SelectFileOptions ): Promise { diff --git a/src/utils/printTemplates.ts b/src/utils/printTemplates.ts index 9f3c4544..2ab4f9b6 100644 --- a/src/utils/printTemplates.ts +++ b/src/utils/printTemplates.ts @@ -3,10 +3,14 @@ import { Doc } from 'fyo/model/doc'; import { Invoice } from 'models/baseModels/Invoice/Invoice'; import { ModelNameEnum } from 'models/types'; import { FieldTypeEnum, Schema, TargetField } from 'schemas/types'; -import { getSavePath, makePDF } from './ipcCalls'; +import { getValueMapFromList } from 'utils/index'; +import { TemplateFile } from 'utils/types'; +import { getSavePath, getTemplates, makePDF } from './ipcCalls'; import { PrintValues } from './types'; +import { getDocFromNameIfExistsElseNew } from './ui'; type PrintTemplateData = Record; +type TemplateUpdateItem = { name: string; template: string; type: string }; const printSettingsFields = [ 'logo', @@ -256,3 +260,92 @@ function getAllCSSAsStyleElem() { styleElem.innerHTML = cssTexts.join('\n'); return styleElem; } + +export async function updatePrintTemplates(fyo: Fyo) { + const templateFiles = await getTemplates(); + const existingTemplates = (await fyo.db.getAll(ModelNameEnum.PrintTemplate, { + fields: ['name', 'modified'], + filters: { isCustom: false }, + })) as { name: string; modified: Date }[]; + + const nameModifiedMap = getValueMapFromList( + existingTemplates, + 'name', + 'modified' + ); + + const updateList: TemplateUpdateItem[] = []; + for (const templateFile of templateFiles) { + const updates = getPrintTemplateUpdateList( + templateFile, + nameModifiedMap, + fyo + ); + + updateList.push(...updates); + } + + for (const { name, type, template } of updateList) { + const doc = await getDocFromNameIfExistsElseNew( + ModelNameEnum.PrintTemplate, + name + ); + + await doc.set({ name, type, template, isCustom: false }); + await doc.sync(); + } +} + +function getPrintTemplateUpdateList( + { file, template, modified: modifiedString }: TemplateFile, + nameModifiedMap: Record, + fyo: Fyo +): TemplateUpdateItem[] { + const templateList: TemplateUpdateItem[] = []; + const dbModified = new Date(modifiedString); + + for (const { name, type } of getNameAndTypeFromTemplateFile(file, fyo)) { + const fileModified = nameModifiedMap[name]; + if (fileModified && dbModified.valueOf() >= fileModified.valueOf()) { + continue; + } + + templateList.push({ + name, + type, + template, + }); + } + return templateList; +} + +function getNameAndTypeFromTemplateFile( + file: string, + fyo: Fyo +): { name: string; type: string }[] { + /** + * Template File Name Format: + * TemplateName[.SchemaName].template.html + * + * If the SchemaName is absent then it is assumed + * that the SchemaName is: + * - SalesInvoice + * - PurchaseInvoice + */ + + const fileName = file.split('.template.html')[0]; + const name = fileName.split('.')[0]; + const schemaName = fileName.split('.')[1]; + + if (schemaName) { + const label = fyo.schemaMap[schemaName]?.label ?? schemaName; + return [{ name: `${name} - ${label}`, type: schemaName }]; + } + + return [ModelNameEnum.SalesInvoice, ModelNameEnum.PurchaseInvoice].map( + (schemaName) => { + const label = fyo.schemaMap[schemaName]?.label ?? schemaName; + return { name: `${name} - ${label}`, type: schemaName }; + } + ); +} diff --git a/fixtures/printTemplates/Basic.template.html b/templates/Basic.template.html similarity index 100% rename from fixtures/printTemplates/Basic.template.html rename to templates/Basic.template.html diff --git a/fixtures/printTemplates/Business.template.html b/templates/Business.template.html similarity index 100% rename from fixtures/printTemplates/Business.template.html rename to templates/Business.template.html diff --git a/fixtures/printTemplates/Minimal.template.html b/templates/Minimal.template.html similarity index 100% rename from fixtures/printTemplates/Minimal.template.html rename to templates/Minimal.template.html diff --git a/utils/messages.ts b/utils/messages.ts index a69d9b48..7340d4fb 100644 --- a/utils/messages.ts +++ b/utils/messages.ts @@ -22,6 +22,7 @@ export enum IPC_ACTIONS { SELECT_FILE = 'select-file', GET_CREDS = 'get-creds', GET_DB_LIST = 'get-db-list', + GET_TEMPLATES = 'get-templates', DELETE_FILE = 'delete-file', // Database messages DB_CREATE = 'db-create', diff --git a/utils/types.ts b/utils/types.ts index 8bdfe203..af9a5e20 100644 --- a/utils/types.ts +++ b/utils/types.ts @@ -23,14 +23,18 @@ export interface VersionParts { beta?: number; } -export type Creds = { errorLogUrl: string; telemetryUrl: string; tokenString: string }; +export type Creds = { + errorLogUrl: string; + telemetryUrl: string; + tokenString: string; +}; export type UnexpectedLogObject = { name: string; message: string; stack: string; more: Record; -} +}; export interface SelectFileOptions { title: string; @@ -48,3 +52,5 @@ export interface SelectFileReturn { export type PropertyEnum> = { [key in keyof Required]: key; }; + +export type TemplateFile = { file: string; template: string; modified: string };