mirror of
https://github.com/frappe/books.git
synced 2025-01-03 15:17:30 +00:00
Merge pull request #687 from frappe/simplify-code-for-db-improvements
feat: create local backups on migrate, use preload
This commit is contained in:
commit
8d8d9ee06e
@ -16,10 +16,14 @@ import {
|
||||
import { DatabaseBase, GetAllOptions, QueryFilter } from '../../utils/db/types';
|
||||
import { getDefaultMetaFieldValueMap, sqliteTypeMap, SYSTEM } from '../helpers';
|
||||
import {
|
||||
AlterConfig,
|
||||
ColumnDiff,
|
||||
FieldValueMap,
|
||||
GetQueryBuilderOptions,
|
||||
MigrationConfig,
|
||||
NonExtantConfig,
|
||||
SingleValue,
|
||||
UpdateSinglesConfig,
|
||||
} from './types';
|
||||
|
||||
/**
|
||||
@ -97,21 +101,73 @@ export default class DatabaseCore extends DatabaseBase {
|
||||
await this.knex!.destroy();
|
||||
}
|
||||
|
||||
async migrate() {
|
||||
for (const schemaName in this.schemaMap) {
|
||||
const schema = this.schemaMap[schemaName] as Schema;
|
||||
if (schema.isSingle) {
|
||||
async migrate(config: MigrationConfig = {}) {
|
||||
const { create, alter } = await this.#getCreateAlterList();
|
||||
const hasSingleValueTable = !create.includes('SingleValue');
|
||||
let singlesConfig: UpdateSinglesConfig = {
|
||||
update: [],
|
||||
updateNonExtant: [],
|
||||
};
|
||||
|
||||
if (hasSingleValueTable) {
|
||||
singlesConfig = await this.#getSinglesUpdateList();
|
||||
}
|
||||
|
||||
const shouldMigrate = !!(
|
||||
create.length ||
|
||||
alter.length ||
|
||||
singlesConfig.update.length ||
|
||||
singlesConfig.updateNonExtant.length
|
||||
);
|
||||
|
||||
if (!shouldMigrate) {
|
||||
return;
|
||||
}
|
||||
|
||||
await config.pre?.();
|
||||
for (const schemaName of create) {
|
||||
await this.#createTable(schemaName);
|
||||
}
|
||||
|
||||
for (const config of alter) {
|
||||
await this.#alterTable(config);
|
||||
}
|
||||
|
||||
if (!hasSingleValueTable) {
|
||||
singlesConfig = await this.#getSinglesUpdateList();
|
||||
}
|
||||
|
||||
await this.#initializeSingles(singlesConfig);
|
||||
await config.post?.();
|
||||
}
|
||||
|
||||
async #getCreateAlterList() {
|
||||
const create: string[] = [];
|
||||
const alter: AlterConfig[] = [];
|
||||
|
||||
for (const [schemaName, schema] of Object.entries(this.schemaMap)) {
|
||||
if (!schema || schema.isSingle) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (await this.#tableExists(schemaName)) {
|
||||
await this.#alterTable(schemaName);
|
||||
} else {
|
||||
await this.#createTable(schemaName);
|
||||
const exists = await this.#tableExists(schemaName);
|
||||
if (!exists) {
|
||||
create.push(schemaName);
|
||||
continue;
|
||||
}
|
||||
|
||||
const diff: ColumnDiff = await this.#getColumnDiff(schemaName);
|
||||
const newForeignKeys: Field[] = await this.#getNewForeignKeys(schemaName);
|
||||
if (diff.added.length || diff.removed.length || newForeignKeys.length) {
|
||||
alter.push({
|
||||
schemaName,
|
||||
diff,
|
||||
newForeignKeys,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await this.#initializeSingles();
|
||||
return { create, alter };
|
||||
}
|
||||
|
||||
async exists(schemaName: string, name?: string): Promise<boolean> {
|
||||
@ -584,11 +640,7 @@ export default class DatabaseCore extends DatabaseBase {
|
||||
}
|
||||
}
|
||||
|
||||
async #alterTable(schemaName: string) {
|
||||
// get columns
|
||||
const diff: ColumnDiff = await this.#getColumnDiff(schemaName);
|
||||
const newForeignKeys: Field[] = await this.#getNewForeignKeys(schemaName);
|
||||
|
||||
async #alterTable({ schemaName, diff, newForeignKeys }: AlterConfig) {
|
||||
await this.knex!.schema.table(schemaName, (table) => {
|
||||
if (!diff.added.length) {
|
||||
return;
|
||||
@ -631,15 +683,17 @@ export default class DatabaseCore extends DatabaseBase {
|
||||
.select('fieldname')) as { fieldname: string }[]
|
||||
).map(({ fieldname }) => fieldname);
|
||||
|
||||
return this.schemaMap[singleSchemaName]!.fields.map(
|
||||
({ fieldname, default: value }) => ({
|
||||
fieldname,
|
||||
value: value,
|
||||
})
|
||||
).filter(
|
||||
({ fieldname, value }) =>
|
||||
!existingFields.includes(fieldname) && value !== undefined
|
||||
);
|
||||
const nonExtant: NonExtantConfig['nonExtant'] = [];
|
||||
const fields = this.schemaMap[singleSchemaName]?.fields ?? [];
|
||||
for (const { fieldname, default: value } of fields) {
|
||||
if (existingFields.includes(fieldname) || value === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
nonExtant.push({ fieldname, value });
|
||||
}
|
||||
|
||||
return nonExtant;
|
||||
}
|
||||
|
||||
async #deleteOne(schemaName: string, name: string) {
|
||||
@ -798,22 +852,42 @@ export default class DatabaseCore extends DatabaseBase {
|
||||
return await this.knex!('SingleValue').insert(fieldValueMap);
|
||||
}
|
||||
|
||||
async #initializeSingles() {
|
||||
const singleSchemaNames = Object.keys(this.schemaMap).filter(
|
||||
(n) => this.schemaMap[n]!.isSingle
|
||||
);
|
||||
|
||||
for (const schemaName of singleSchemaNames) {
|
||||
if (await this.#singleExists(schemaName)) {
|
||||
await this.#updateNonExtantSingleValues(schemaName);
|
||||
async #getSinglesUpdateList() {
|
||||
const update: string[] = [];
|
||||
const updateNonExtant: NonExtantConfig[] = [];
|
||||
for (const [schemaName, schema] of Object.entries(this.schemaMap)) {
|
||||
if (!schema || !schema.isSingle) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const exists = await this.#singleExists(schemaName);
|
||||
if (!exists && schema.fields.some((f) => f.default !== undefined)) {
|
||||
update.push(schemaName);
|
||||
}
|
||||
|
||||
if (!exists) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const nonExtant = await this.#getNonExtantSingleValues(schemaName);
|
||||
if (nonExtant.length) {
|
||||
updateNonExtant.push({
|
||||
schemaName,
|
||||
nonExtant,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return { update, updateNonExtant };
|
||||
}
|
||||
|
||||
async #initializeSingles({ update, updateNonExtant }: UpdateSinglesConfig) {
|
||||
for (const config of updateNonExtant) {
|
||||
await this.#updateNonExtantSingleValues(config);
|
||||
}
|
||||
|
||||
for (const schemaName of update) {
|
||||
const fields = this.schemaMap[schemaName]!.fields;
|
||||
if (fields.every((f) => f.default === undefined)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const defaultValues: FieldValueMap = fields.reduce((acc, f) => {
|
||||
if (f.default !== undefined) {
|
||||
acc[f.fieldname] = f.default;
|
||||
@ -826,10 +900,12 @@ export default class DatabaseCore extends DatabaseBase {
|
||||
}
|
||||
}
|
||||
|
||||
async #updateNonExtantSingleValues(schemaName: string) {
|
||||
const singleValues = await this.#getNonExtantSingleValues(schemaName);
|
||||
for (const sv of singleValues) {
|
||||
await this.#updateSingleValue(schemaName, sv.fieldname, sv.value!);
|
||||
async #updateNonExtantSingleValues({
|
||||
schemaName,
|
||||
nonExtant,
|
||||
}: NonExtantConfig) {
|
||||
for (const { fieldname, value } of nonExtant) {
|
||||
await this.#updateSingleValue(schemaName, fieldname, value);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,9 +1,12 @@
|
||||
import fs from 'fs/promises';
|
||||
import BetterSQLite3 from 'better-sqlite3';
|
||||
import fs from 'fs-extra';
|
||||
import { DatabaseError } from 'fyo/utils/errors';
|
||||
import path from 'path';
|
||||
import { DatabaseDemuxBase, DatabaseMethod } from 'utils/db/types';
|
||||
import { getMapFromList } from 'utils/index';
|
||||
import { Version } from 'utils/version';
|
||||
import { getSchemas } from '../../schemas';
|
||||
import { checkFileAccess, databaseMethodSet, unlinkIfExists } from '../helpers';
|
||||
import { databaseMethodSet, unlinkIfExists } from '../helpers';
|
||||
import patches from '../patches';
|
||||
import { BespokeQueries } from './bespoke';
|
||||
import DatabaseCore from './core';
|
||||
@ -55,66 +58,70 @@ export class DatabaseManager extends DatabaseDemuxBase {
|
||||
await this.db!.migrate();
|
||||
}
|
||||
|
||||
/**
|
||||
* This needs to be supplimented with transactions
|
||||
* TODO: Add transactions in core.ts
|
||||
*/
|
||||
const dbPath = this.db!.dbPath;
|
||||
const copyPath = await this.#makeTempCopy();
|
||||
|
||||
try {
|
||||
await this.#runPatchesAndMigrate();
|
||||
} catch (error) {
|
||||
await this.#handleFailedMigration(error, dbPath, copyPath);
|
||||
} finally {
|
||||
await unlinkIfExists(copyPath);
|
||||
}
|
||||
await this.#executeMigration();
|
||||
}
|
||||
|
||||
async #handleFailedMigration(
|
||||
error: unknown,
|
||||
dbPath: string,
|
||||
copyPath: string | null
|
||||
) {
|
||||
await this.db!.close();
|
||||
async #executeMigration() {
|
||||
const version = await this.#getAppVersion();
|
||||
const patches = await this.#getPatchesToExecute(version);
|
||||
|
||||
if (copyPath && (await checkFileAccess(copyPath))) {
|
||||
await fs.copyFile(copyPath, dbPath);
|
||||
const hasPatches = !!patches.pre.length || !!patches.post.length;
|
||||
if (hasPatches) {
|
||||
await this.#createBackup();
|
||||
}
|
||||
|
||||
if (error instanceof Error) {
|
||||
error.message = `failed migration\n${error.message}`;
|
||||
}
|
||||
await runPatches(patches.pre, this, version);
|
||||
await this.db!.migrate({
|
||||
pre: async () => {
|
||||
if (hasPatches) {
|
||||
return;
|
||||
}
|
||||
|
||||
throw error;
|
||||
await this.#createBackup();
|
||||
},
|
||||
});
|
||||
await runPatches(patches.post, this, version);
|
||||
}
|
||||
|
||||
async #runPatchesAndMigrate() {
|
||||
const patchesToExecute = await this.#getPatchesToExecute();
|
||||
|
||||
patchesToExecute.sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
|
||||
const preMigrationPatches = patchesToExecute.filter(
|
||||
(p) => p.patch.beforeMigrate
|
||||
);
|
||||
const postMigrationPatches = patchesToExecute.filter(
|
||||
(p) => !p.patch.beforeMigrate
|
||||
);
|
||||
|
||||
await runPatches(preMigrationPatches, this);
|
||||
await this.db!.migrate();
|
||||
await runPatches(postMigrationPatches, this);
|
||||
}
|
||||
|
||||
async #getPatchesToExecute(): Promise<Patch[]> {
|
||||
async #getPatchesToExecute(
|
||||
version: string
|
||||
): Promise<{ pre: Patch[]; post: Patch[] }> {
|
||||
if (this.db === undefined) {
|
||||
return [];
|
||||
return { pre: [], post: [] };
|
||||
}
|
||||
|
||||
const query: { name: string }[] = await this.db.knex!('PatchRun').select(
|
||||
'name'
|
||||
);
|
||||
const executedPatches = query.map((q) => q.name);
|
||||
return patches.filter((p) => !executedPatches.includes(p.name));
|
||||
const query = (await this.db.knex!('PatchRun').select()) as {
|
||||
name: string;
|
||||
version?: string;
|
||||
failed?: boolean;
|
||||
}[];
|
||||
|
||||
const runPatchesMap = getMapFromList(query, 'name');
|
||||
/**
|
||||
* A patch is run only if:
|
||||
* - it hasn't run and was added in a future version
|
||||
* i.e. app version is before patch added version
|
||||
* - it ran but failed in some other version (i.e fixed)
|
||||
*/
|
||||
const filtered = patches
|
||||
.filter((p) => {
|
||||
const exec = runPatchesMap[p.name];
|
||||
if (!exec && Version.lte(version, p.version)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (exec?.failed && exec?.version !== version) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
})
|
||||
.sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
|
||||
|
||||
return {
|
||||
pre: filtered.filter((p) => p.patch.beforeMigrate),
|
||||
post: filtered.filter((p) => !p.patch.beforeMigrate),
|
||||
};
|
||||
}
|
||||
|
||||
async call(method: DatabaseMethod, ...args: unknown[]) {
|
||||
@ -150,32 +157,72 @@ export class DatabaseManager extends DatabaseDemuxBase {
|
||||
}
|
||||
|
||||
async #getIsFirstRun(): Promise<boolean> {
|
||||
if (!this.#isInitialized) {
|
||||
const knex = this.db?.knex;
|
||||
if (!knex) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const tableList: unknown[] = await this.db!.knex!.raw(
|
||||
"SELECT name FROM sqlite_master WHERE type='table'"
|
||||
);
|
||||
return tableList.length === 0;
|
||||
const query = await knex('sqlite_master').where({
|
||||
type: 'table',
|
||||
name: 'PatchRun',
|
||||
});
|
||||
return !query.length;
|
||||
}
|
||||
|
||||
async #makeTempCopy() {
|
||||
const src = this.db!.dbPath;
|
||||
if (src === ':memory:') {
|
||||
async #createBackup() {
|
||||
const { dbPath } = this.db ?? {};
|
||||
if (!dbPath || process.env.IS_TEST) {
|
||||
return;
|
||||
}
|
||||
|
||||
const backupPath = await this.#getBackupFilePath();
|
||||
if (!backupPath) {
|
||||
return;
|
||||
}
|
||||
|
||||
const db = this.getDriver();
|
||||
await db?.backup(backupPath).then(() => db.close());
|
||||
}
|
||||
|
||||
async #getBackupFilePath() {
|
||||
const { dbPath } = this.db ?? {};
|
||||
if (dbPath === ':memory:' || !dbPath) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const dir = path.parse(src).dir;
|
||||
const dest = path.join(dir, '__premigratory_temp.db');
|
||||
let fileName = path.parse(dbPath).name;
|
||||
if (fileName.endsWith('.books')) {
|
||||
fileName = fileName.slice(0, -6);
|
||||
}
|
||||
|
||||
try {
|
||||
await fs.copyFile(src, dest);
|
||||
} catch (err) {
|
||||
const backupFolder = path.join(path.dirname(dbPath), 'backups');
|
||||
const date = new Date().toISOString().split('T')[0];
|
||||
const version = await this.#getAppVersion();
|
||||
const backupFile = `${fileName}-${version}-${date}.books.db`;
|
||||
fs.ensureDirSync(backupFolder);
|
||||
return path.join(backupFolder, backupFile);
|
||||
}
|
||||
|
||||
async #getAppVersion(): Promise<string> {
|
||||
const knex = this.db?.knex;
|
||||
if (!knex) {
|
||||
return '0.0.0';
|
||||
}
|
||||
|
||||
const query = await knex('SingleValue')
|
||||
.select('value')
|
||||
.where({ fieldname: 'version', parent: 'SystemSettings' });
|
||||
const value = (query[0] as undefined | { value: string })?.value;
|
||||
return value || '0.0.0';
|
||||
}
|
||||
|
||||
getDriver() {
|
||||
const { dbPath } = this.db ?? {};
|
||||
if (!dbPath) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return dest;
|
||||
return BetterSQLite3(dbPath, { readonly: true });
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2,34 +2,50 @@ import { emitMainProcessError, getDefaultMetaFieldValueMap } from '../helpers';
|
||||
import { DatabaseManager } from './manager';
|
||||
import { FieldValueMap, Patch } from './types';
|
||||
|
||||
export async function runPatches(patches: Patch[], dm: DatabaseManager) {
|
||||
export async function runPatches(
|
||||
patches: Patch[],
|
||||
dm: DatabaseManager,
|
||||
version: string
|
||||
) {
|
||||
const list: { name: string; success: boolean }[] = [];
|
||||
for (const patch of patches) {
|
||||
const success = await runPatch(patch, dm);
|
||||
const success = await runPatch(patch, dm, version);
|
||||
list.push({ name: patch.name, success });
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
async function runPatch(patch: Patch, dm: DatabaseManager): Promise<boolean> {
|
||||
async function runPatch(
|
||||
patch: Patch,
|
||||
dm: DatabaseManager,
|
||||
version: string
|
||||
): Promise<boolean> {
|
||||
let failed = false;
|
||||
try {
|
||||
await patch.patch.execute(dm);
|
||||
} catch (error) {
|
||||
if (!(error instanceof Error)) {
|
||||
return false;
|
||||
failed = true;
|
||||
if (error instanceof Error) {
|
||||
error.message = `Patch Failed: ${patch.name}\n${error.message}`;
|
||||
emitMainProcessError(error, { patchName: patch.name, notifyUser: false });
|
||||
}
|
||||
|
||||
error.message = `Patch Failed: ${patch.name}\n${error.message}`;
|
||||
emitMainProcessError(error, { patchName: patch.name, notifyUser: false });
|
||||
return false;
|
||||
}
|
||||
|
||||
await makeEntry(patch.name, dm);
|
||||
await makeEntry(patch.name, version, failed, dm);
|
||||
return true;
|
||||
}
|
||||
|
||||
async function makeEntry(patchName: string, dm: DatabaseManager) {
|
||||
async function makeEntry(
|
||||
patchName: string,
|
||||
version: string,
|
||||
failed: boolean,
|
||||
dm: DatabaseManager
|
||||
) {
|
||||
const defaultFieldValueMap = getDefaultMetaFieldValueMap() as FieldValueMap;
|
||||
|
||||
defaultFieldValueMap.name = patchName;
|
||||
defaultFieldValueMap.failed = failed;
|
||||
defaultFieldValueMap.version = version;
|
||||
|
||||
await dm.db!.insert('PatchRun', defaultFieldValueMap);
|
||||
}
|
||||
|
@ -16,6 +16,30 @@ export type FieldValueMap = Record<
|
||||
RawValue | undefined | FieldValueMap[]
|
||||
>;
|
||||
|
||||
export type AlterConfig = {
|
||||
schemaName: string;
|
||||
diff: ColumnDiff;
|
||||
newForeignKeys: Field[];
|
||||
};
|
||||
|
||||
export type NonExtantConfig = {
|
||||
schemaName: string;
|
||||
nonExtant: {
|
||||
fieldname: string;
|
||||
value: RawValue;
|
||||
}[];
|
||||
};
|
||||
|
||||
export type UpdateSinglesConfig = {
|
||||
update: string[];
|
||||
updateNonExtant: NonExtantConfig[];
|
||||
};
|
||||
|
||||
export type MigrationConfig = {
|
||||
pre?: () => Promise<void> | void;
|
||||
post?: () => Promise<void> | void;
|
||||
};
|
||||
|
||||
export interface Patch {
|
||||
name: string;
|
||||
version: string;
|
||||
@ -50,6 +74,7 @@ export type BespokeFunction = (
|
||||
db: DatabaseCore,
|
||||
...args: unknown[]
|
||||
) => Promise<unknown>;
|
||||
|
||||
export type SingleValue<T> = {
|
||||
fieldname: string;
|
||||
parent: string;
|
||||
|
@ -51,7 +51,7 @@ function updatePaths() {
|
||||
async function buildMainProcessSource() {
|
||||
const result = await esbuild.build({
|
||||
...commonConfig,
|
||||
outfile: path.join(buildDirPath, mainFileName),
|
||||
outdir: path.join(buildDirPath),
|
||||
});
|
||||
|
||||
if (result.errors.length) {
|
||||
|
@ -42,7 +42,7 @@ const viteProcess = $$`yarn vite`;
|
||||
*/
|
||||
const ctx = await esbuild.context({
|
||||
...getMainProcessCommonConfig(root),
|
||||
outfile: path.join(root, 'dist_electron', 'dev', 'main.js'),
|
||||
outdir: path.join(root, 'dist_electron', 'dev'),
|
||||
});
|
||||
|
||||
/**
|
||||
|
@ -10,7 +10,10 @@ import path from 'path';
|
||||
*/
|
||||
export function getMainProcessCommonConfig(root) {
|
||||
return {
|
||||
entryPoints: [path.join(root, 'main.ts')],
|
||||
entryPoints: [
|
||||
path.join(root, 'main.ts'),
|
||||
path.join(root, 'main', 'preload.ts'),
|
||||
],
|
||||
bundle: true,
|
||||
sourcemap: true,
|
||||
sourcesContent: false,
|
||||
|
@ -1,7 +1,5 @@
|
||||
import { AuthDemuxBase } from 'utils/auth/types';
|
||||
import { IPC_ACTIONS } from 'utils/messages';
|
||||
import { Creds } from 'utils/types';
|
||||
const { ipcRenderer } = require('electron');
|
||||
|
||||
export class AuthDemux extends AuthDemuxBase {
|
||||
#isElectron = false;
|
||||
@ -12,7 +10,7 @@ export class AuthDemux extends AuthDemuxBase {
|
||||
|
||||
async getCreds(): Promise<Creds> {
|
||||
if (this.#isElectron) {
|
||||
return (await ipcRenderer.invoke(IPC_ACTIONS.GET_CREDS)) as Creds;
|
||||
return await ipc.getCreds();
|
||||
} else {
|
||||
return { errorLogUrl: '', tokenString: '', telemetryUrl: '' };
|
||||
}
|
||||
|
@ -1,26 +1,12 @@
|
||||
import type Store from 'electron-store';
|
||||
import { ConfigMap } from 'fyo/core/types';
|
||||
import type { IPC } from 'main/preload';
|
||||
|
||||
export class Config {
|
||||
config: Map<string, unknown> | Store;
|
||||
config: Map<string, unknown> | IPC['store'];
|
||||
constructor(isElectron: boolean) {
|
||||
this.config = new Map();
|
||||
if (isElectron) {
|
||||
const Config = require('electron-store') as typeof Store;
|
||||
this.config = new Config();
|
||||
}
|
||||
}
|
||||
|
||||
get store() {
|
||||
if (this.config instanceof Map) {
|
||||
const store: Record<string, unknown> = {};
|
||||
for (const key of this.config.keys()) {
|
||||
store[key] = this.config.get(key);
|
||||
}
|
||||
|
||||
return store;
|
||||
} else {
|
||||
return this.config;
|
||||
this.config = ipc.store;
|
||||
}
|
||||
}
|
||||
|
||||
@ -39,8 +25,4 @@ export class Config {
|
||||
delete(key: keyof ConfigMap) {
|
||||
this.config.delete(key);
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.config.clear();
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,7 @@
|
||||
const { ipcRenderer } = require('electron');
|
||||
import { DatabaseError, NotImplemented } from 'fyo/utils/errors';
|
||||
import { SchemaMap } from 'schemas/types';
|
||||
import { DatabaseDemuxBase, DatabaseMethod } from 'utils/db/types';
|
||||
import { BackendResponse } from 'utils/ipc/types';
|
||||
import { IPC_ACTIONS } from 'utils/messages';
|
||||
|
||||
export class DatabaseDemux extends DatabaseDemuxBase {
|
||||
#isElectron = false;
|
||||
@ -32,9 +30,7 @@ export class DatabaseDemux extends DatabaseDemuxBase {
|
||||
}
|
||||
|
||||
return (await this.#handleDBCall(async () => {
|
||||
return (await ipcRenderer.invoke(
|
||||
IPC_ACTIONS.DB_SCHEMA
|
||||
)) as BackendResponse;
|
||||
return await ipc.db.getSchema();
|
||||
})) as SchemaMap;
|
||||
}
|
||||
|
||||
@ -47,11 +43,7 @@ export class DatabaseDemux extends DatabaseDemuxBase {
|
||||
}
|
||||
|
||||
return (await this.#handleDBCall(async () => {
|
||||
return (await ipcRenderer.invoke(
|
||||
IPC_ACTIONS.DB_CREATE,
|
||||
dbPath,
|
||||
countryCode
|
||||
)) as BackendResponse;
|
||||
return ipc.db.create(dbPath, countryCode);
|
||||
})) as string;
|
||||
}
|
||||
|
||||
@ -64,11 +56,7 @@ export class DatabaseDemux extends DatabaseDemuxBase {
|
||||
}
|
||||
|
||||
return (await this.#handleDBCall(async () => {
|
||||
return (await ipcRenderer.invoke(
|
||||
IPC_ACTIONS.DB_CONNECT,
|
||||
dbPath,
|
||||
countryCode
|
||||
)) as BackendResponse;
|
||||
return ipc.db.connect(dbPath, countryCode);
|
||||
})) as string;
|
||||
}
|
||||
|
||||
@ -78,11 +66,7 @@ export class DatabaseDemux extends DatabaseDemuxBase {
|
||||
}
|
||||
|
||||
return await this.#handleDBCall(async () => {
|
||||
return (await ipcRenderer.invoke(
|
||||
IPC_ACTIONS.DB_CALL,
|
||||
method,
|
||||
...args
|
||||
)) as BackendResponse;
|
||||
return await ipc.db.call(method, ...args);
|
||||
});
|
||||
}
|
||||
|
||||
@ -92,11 +76,7 @@ export class DatabaseDemux extends DatabaseDemuxBase {
|
||||
}
|
||||
|
||||
return await this.#handleDBCall(async () => {
|
||||
return (await ipcRenderer.invoke(
|
||||
IPC_ACTIONS.DB_BESPOKE,
|
||||
method,
|
||||
...args
|
||||
)) as BackendResponse;
|
||||
return await ipc.db.bespoke(method, ...args);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -96,7 +96,7 @@ export class Fyo {
|
||||
|
||||
setIsElectron() {
|
||||
try {
|
||||
this.isElectron = Boolean(require('electron'));
|
||||
this.isElectron = !!window?.ipc;
|
||||
} catch {
|
||||
this.isElectron = false;
|
||||
}
|
||||
|
12
main.ts
12
main.ts
@ -4,6 +4,7 @@ require('source-map-support').install({
|
||||
environment: 'node',
|
||||
});
|
||||
|
||||
import { emitMainProcessError } from 'backend/helpers';
|
||||
import {
|
||||
app,
|
||||
BrowserWindow,
|
||||
@ -12,7 +13,6 @@ import {
|
||||
ProtocolRequest,
|
||||
ProtocolResponse,
|
||||
} from 'electron';
|
||||
import Store from 'electron-store';
|
||||
import { autoUpdater } from 'electron-updater';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
@ -21,7 +21,6 @@ import registerAutoUpdaterListeners from './main/registerAutoUpdaterListeners';
|
||||
import registerIpcMainActionListeners from './main/registerIpcMainActionListeners';
|
||||
import registerIpcMainMessageListeners from './main/registerIpcMainMessageListeners';
|
||||
import registerProcessListeners from './main/registerProcessListeners';
|
||||
import { emitMainProcessError } from 'backend/helpers';
|
||||
|
||||
export class Main {
|
||||
title = 'Frappe Books';
|
||||
@ -54,8 +53,6 @@ export class Main {
|
||||
'no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0',
|
||||
};
|
||||
|
||||
Store.initRenderer();
|
||||
|
||||
this.registerListeners();
|
||||
if (this.isMac && this.isDevelopment) {
|
||||
app.dock.setIcon(this.icon);
|
||||
@ -87,6 +84,7 @@ export class Main {
|
||||
}
|
||||
|
||||
getOptions(): BrowserWindowConstructorOptions {
|
||||
const preload = path.join(__dirname, 'main', 'preload.js');
|
||||
const options: BrowserWindowConstructorOptions = {
|
||||
width: this.WIDTH,
|
||||
height: this.HEIGHT,
|
||||
@ -94,8 +92,10 @@ export class Main {
|
||||
titleBarStyle: 'hidden',
|
||||
trafficLightPosition: { x: 16, y: 16 },
|
||||
webPreferences: {
|
||||
contextIsolation: false, // TODO: Switch this off
|
||||
nodeIntegration: true,
|
||||
contextIsolation: true,
|
||||
nodeIntegration: false,
|
||||
sandbox: false,
|
||||
preload,
|
||||
},
|
||||
autoHideMenuBar: true,
|
||||
frame: !this.isMac,
|
||||
|
211
main/preload.ts
Normal file
211
main/preload.ts
Normal file
@ -0,0 +1,211 @@
|
||||
import type {
|
||||
OpenDialogOptions,
|
||||
OpenDialogReturnValue,
|
||||
SaveDialogOptions,
|
||||
SaveDialogReturnValue,
|
||||
} from 'electron';
|
||||
import { contextBridge, ipcRenderer } from 'electron';
|
||||
import type { ConfigMap } from 'fyo/core/types';
|
||||
import config from 'utils/config';
|
||||
import type { DatabaseMethod } from 'utils/db/types';
|
||||
import type { BackendResponse } from 'utils/ipc/types';
|
||||
import { IPC_ACTIONS, IPC_CHANNELS, IPC_MESSAGES } from 'utils/messages';
|
||||
import type {
|
||||
ConfigFilesWithModified,
|
||||
Creds,
|
||||
LanguageMap,
|
||||
SelectFileOptions,
|
||||
SelectFileReturn,
|
||||
TemplateFile,
|
||||
} from 'utils/types';
|
||||
|
||||
type IPCRendererListener = Parameters<typeof ipcRenderer.on>[1];
|
||||
const ipc = {
|
||||
desktop: true,
|
||||
|
||||
reloadWindow() {
|
||||
return ipcRenderer.send(IPC_MESSAGES.RELOAD_MAIN_WINDOW);
|
||||
},
|
||||
|
||||
async getCreds() {
|
||||
return (await ipcRenderer.invoke(IPC_ACTIONS.GET_CREDS)) as Creds;
|
||||
},
|
||||
|
||||
async getLanguageMap(code: string) {
|
||||
return (await ipcRenderer.invoke(IPC_ACTIONS.GET_LANGUAGE_MAP, code)) as {
|
||||
languageMap: LanguageMap;
|
||||
success: boolean;
|
||||
message: string;
|
||||
};
|
||||
},
|
||||
|
||||
async getTemplates(): Promise<TemplateFile[]> {
|
||||
return (await ipcRenderer.invoke(
|
||||
IPC_ACTIONS.GET_TEMPLATES
|
||||
)) as TemplateFile[];
|
||||
},
|
||||
|
||||
async selectFile(options: SelectFileOptions): Promise<SelectFileReturn> {
|
||||
return (await ipcRenderer.invoke(
|
||||
IPC_ACTIONS.SELECT_FILE,
|
||||
options
|
||||
)) as SelectFileReturn;
|
||||
},
|
||||
|
||||
async getSaveFilePath(options: SaveDialogOptions) {
|
||||
return (await ipcRenderer.invoke(
|
||||
IPC_ACTIONS.GET_SAVE_FILEPATH,
|
||||
options
|
||||
)) as SaveDialogReturnValue;
|
||||
},
|
||||
|
||||
async getOpenFilePath(options: OpenDialogOptions) {
|
||||
return (await ipcRenderer.invoke(
|
||||
IPC_ACTIONS.GET_OPEN_FILEPATH,
|
||||
options
|
||||
)) as OpenDialogReturnValue;
|
||||
},
|
||||
|
||||
async checkDbAccess(filePath: string) {
|
||||
return (await ipcRenderer.invoke(
|
||||
IPC_ACTIONS.CHECK_DB_ACCESS,
|
||||
filePath
|
||||
)) as boolean;
|
||||
},
|
||||
|
||||
async checkForUpdates() {
|
||||
await ipcRenderer.invoke(IPC_ACTIONS.CHECK_FOR_UPDATES);
|
||||
},
|
||||
|
||||
openLink(link: string) {
|
||||
ipcRenderer.send(IPC_MESSAGES.OPEN_EXTERNAL, link);
|
||||
},
|
||||
|
||||
async deleteFile(filePath: string) {
|
||||
return (await ipcRenderer.invoke(
|
||||
IPC_ACTIONS.DELETE_FILE,
|
||||
filePath
|
||||
)) as BackendResponse;
|
||||
},
|
||||
|
||||
async saveData(data: string, savePath: string) {
|
||||
await ipcRenderer.invoke(IPC_ACTIONS.SAVE_DATA, data, savePath);
|
||||
},
|
||||
|
||||
showItemInFolder(filePath: string) {
|
||||
ipcRenderer.send(IPC_MESSAGES.SHOW_ITEM_IN_FOLDER, filePath);
|
||||
},
|
||||
|
||||
async makePDF(
|
||||
html: string,
|
||||
savePath: string,
|
||||
width: number,
|
||||
height: number
|
||||
): Promise<boolean> {
|
||||
return (await ipcRenderer.invoke(
|
||||
IPC_ACTIONS.SAVE_HTML_AS_PDF,
|
||||
html,
|
||||
savePath,
|
||||
width,
|
||||
height
|
||||
)) as boolean;
|
||||
},
|
||||
|
||||
async getDbList() {
|
||||
return (await ipcRenderer.invoke(
|
||||
IPC_ACTIONS.GET_DB_LIST
|
||||
)) as ConfigFilesWithModified[];
|
||||
},
|
||||
|
||||
async getDbDefaultPath(companyName: string) {
|
||||
return (await ipcRenderer.invoke(
|
||||
IPC_ACTIONS.GET_DB_DEFAULT_PATH,
|
||||
companyName
|
||||
)) as string;
|
||||
},
|
||||
|
||||
async getEnv() {
|
||||
return (await ipcRenderer.invoke(IPC_ACTIONS.GET_ENV)) as {
|
||||
isDevelopment: boolean;
|
||||
platform: string;
|
||||
version: string;
|
||||
};
|
||||
},
|
||||
|
||||
openExternalUrl(url: string) {
|
||||
ipcRenderer.send(IPC_MESSAGES.OPEN_EXTERNAL, url);
|
||||
},
|
||||
|
||||
async showError(title: string, content: string) {
|
||||
await ipcRenderer.invoke(IPC_ACTIONS.SHOW_ERROR, { title, content });
|
||||
},
|
||||
|
||||
async sendError(body: string) {
|
||||
await ipcRenderer.invoke(IPC_ACTIONS.SEND_ERROR, body);
|
||||
},
|
||||
|
||||
registerMainProcessErrorListener(listener: IPCRendererListener) {
|
||||
ipcRenderer.on(IPC_CHANNELS.LOG_MAIN_PROCESS_ERROR, listener);
|
||||
},
|
||||
|
||||
registerConsoleLogListener(listener: IPCRendererListener) {
|
||||
ipcRenderer.on(IPC_CHANNELS.CONSOLE_LOG, listener);
|
||||
},
|
||||
|
||||
db: {
|
||||
async getSchema() {
|
||||
return (await ipcRenderer.invoke(
|
||||
IPC_ACTIONS.DB_SCHEMA
|
||||
)) as BackendResponse;
|
||||
},
|
||||
|
||||
async create(dbPath: string, countryCode?: string) {
|
||||
return (await ipcRenderer.invoke(
|
||||
IPC_ACTIONS.DB_CREATE,
|
||||
dbPath,
|
||||
countryCode
|
||||
)) as BackendResponse;
|
||||
},
|
||||
|
||||
async connect(dbPath: string, countryCode?: string) {
|
||||
return (await ipcRenderer.invoke(
|
||||
IPC_ACTIONS.DB_CONNECT,
|
||||
dbPath,
|
||||
countryCode
|
||||
)) as BackendResponse;
|
||||
},
|
||||
|
||||
async call(method: DatabaseMethod, ...args: unknown[]) {
|
||||
return (await ipcRenderer.invoke(
|
||||
IPC_ACTIONS.DB_CALL,
|
||||
method,
|
||||
...args
|
||||
)) as BackendResponse;
|
||||
},
|
||||
|
||||
async bespoke(method: string, ...args: unknown[]) {
|
||||
return (await ipcRenderer.invoke(
|
||||
IPC_ACTIONS.DB_BESPOKE,
|
||||
method,
|
||||
...args
|
||||
)) as BackendResponse;
|
||||
},
|
||||
},
|
||||
|
||||
store: {
|
||||
get<K extends keyof ConfigMap>(key: K) {
|
||||
return config.get(key);
|
||||
},
|
||||
|
||||
set<K extends keyof ConfigMap>(key: K, value: ConfigMap[K]) {
|
||||
return config.set(key, value);
|
||||
},
|
||||
|
||||
delete(key: keyof ConfigMap) {
|
||||
return config.delete(key);
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
contextBridge.exposeInMainWorld('ipc', ipc);
|
||||
export type IPC = typeof ipc;
|
@ -8,7 +8,7 @@ import {
|
||||
} from 'electron';
|
||||
import { autoUpdater } from 'electron-updater';
|
||||
import { constants } from 'fs';
|
||||
import fs from 'fs/promises';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
import { SelectFileOptions, SelectFileReturn } from 'utils/types';
|
||||
import databaseManager from '../backend/database/manager';
|
||||
@ -38,6 +38,22 @@ export default function registerIpcMainActionListeners(main: Main) {
|
||||
return true;
|
||||
});
|
||||
|
||||
ipcMain.handle(
|
||||
IPC_ACTIONS.GET_DB_DEFAULT_PATH,
|
||||
async (_, companyName: string) => {
|
||||
let root = app.getPath('documents');
|
||||
if (main.isDevelopment) {
|
||||
root = 'dbs';
|
||||
}
|
||||
|
||||
const dbsPath = path.join(root, 'Frappe Books');
|
||||
const backupPath = path.join(dbsPath, 'backups');
|
||||
await fs.ensureDir(backupPath);
|
||||
|
||||
return path.join(dbsPath, `${companyName}.books.db`);
|
||||
}
|
||||
);
|
||||
|
||||
ipcMain.handle(
|
||||
IPC_ACTIONS.GET_OPEN_FILEPATH,
|
||||
async (_, options: OpenDialogOptions) => {
|
||||
@ -169,11 +185,17 @@ export default function registerIpcMainActionListeners(main: Main) {
|
||||
return await getConfigFilesWithModified(files);
|
||||
});
|
||||
|
||||
ipcMain.handle(IPC_ACTIONS.GET_ENV, () => {
|
||||
ipcMain.handle(IPC_ACTIONS.GET_ENV, async () => {
|
||||
let version = app.getVersion();
|
||||
if (main.isDevelopment) {
|
||||
const packageJson = await fs.readFile('package.json', 'utf-8');
|
||||
version = (JSON.parse(packageJson) as { version: string }).version;
|
||||
}
|
||||
|
||||
return {
|
||||
isDevelopment: main.isDevelopment,
|
||||
platform: process.platform,
|
||||
version: app.getVersion(),
|
||||
version,
|
||||
};
|
||||
});
|
||||
|
||||
|
@ -43,6 +43,7 @@
|
||||
"@codemirror/view": "^6.0.0",
|
||||
"@lezer/common": "^1.0.0",
|
||||
"@types/assert": "^1.5.6",
|
||||
"@types/better-sqlite3": "^7.6.4",
|
||||
"@types/electron-devtools-installer": "^2.2.0",
|
||||
"@types/lodash": "^4.14.179",
|
||||
"@types/luxon": "^2.3.1",
|
||||
|
@ -5,11 +5,11 @@ import { ModelNameEnum } from 'models/types';
|
||||
import { codeStateMap } from 'regional/in';
|
||||
import { ExportExtention } from 'reports/types';
|
||||
import { showDialog } from 'src/utils/interactive';
|
||||
import { getSavePath } from 'src/utils/ipcCalls';
|
||||
import { invertMap } from 'utils';
|
||||
import { getCsvData, saveExportData } from '../commonExporter';
|
||||
import { BaseGSTR } from './BaseGSTR';
|
||||
import { TransferTypeEnum } from './types';
|
||||
import { getSavePath } from 'src/utils/ui';
|
||||
|
||||
const GST = {
|
||||
'GST-0': 0,
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { t } from 'fyo';
|
||||
import { Action } from 'fyo/model/types';
|
||||
import { Verb } from 'fyo/telemetry/types';
|
||||
import { getSavePath, saveData, showExportInFolder } from 'src/utils/ipcCalls';
|
||||
import { getSavePath, showExportInFolder } from 'src/utils/ui';
|
||||
import { getIsNullOrUndef } from 'utils';
|
||||
import { generateCSV } from 'utils/csvParser';
|
||||
import { Report } from './Report';
|
||||
@ -183,7 +183,7 @@ export async function saveExportData(
|
||||
filePath: string,
|
||||
message?: string
|
||||
) {
|
||||
await saveData(data, filePath);
|
||||
await ipc.saveData(data, filePath);
|
||||
message ??= t`Export Successful`;
|
||||
showExportInFolder(message, filePath);
|
||||
}
|
||||
|
@ -8,6 +8,18 @@
|
||||
"fieldtype": "Data",
|
||||
"label": "Name",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"fieldname": "failed",
|
||||
"fieldtype": "Check",
|
||||
"label": "Failed",
|
||||
"default": false
|
||||
},
|
||||
{
|
||||
"fieldname": "version",
|
||||
"fieldtype": "Data",
|
||||
"label": "Version",
|
||||
"default": "0.0.0"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -60,6 +60,7 @@
|
||||
"fieldname": "version",
|
||||
"label": "Version",
|
||||
"fieldtype": "Data",
|
||||
"default": "0.0.0",
|
||||
"readOnly": true,
|
||||
"section": "Default"
|
||||
},
|
||||
|
@ -5,4 +5,5 @@ if [ $# -eq 0 ]
|
||||
TEST_PATH=./**/tests/**/*.spec.ts
|
||||
fi
|
||||
|
||||
export IS_TEST=true
|
||||
./scripts/runner.sh ./node_modules/.bin/tape $TEST_PATH | ./node_modules/.bin/tap-spec
|
28
src/App.vue
28
src/App.vue
@ -19,6 +19,7 @@
|
||||
<DatabaseSelector
|
||||
v-if="activeScreen === 'DatabaseSelector'"
|
||||
ref="databaseSelector"
|
||||
@new-database="newDatabase"
|
||||
@file-selected="fileSelected"
|
||||
/>
|
||||
<SetupWizard
|
||||
@ -53,7 +54,7 @@ import { connectToDatabase, dbErrorActionSymbols } from './utils/db';
|
||||
import { initializeInstance } from './utils/initialization';
|
||||
import * as injectionKeys from './utils/injectionKeys';
|
||||
import { showDialog } from './utils/interactive';
|
||||
import { checkDbAccess, checkForUpdates } from './utils/ipcCalls';
|
||||
import { setLanguageMap } from './utils/language';
|
||||
import { updateConfigFiles } from './utils/misc';
|
||||
import { updatePrintTemplates } from './utils/printTemplates';
|
||||
import { Search } from './utils/search';
|
||||
@ -136,7 +137,7 @@ export default defineComponent({
|
||||
return;
|
||||
}
|
||||
|
||||
await this.fileSelected(lastSelectedFilePath, false);
|
||||
await this.fileSelected(lastSelectedFilePath);
|
||||
},
|
||||
async setSearcher(): Promise<void> {
|
||||
this.searcher = new Search(fyo);
|
||||
@ -146,7 +147,8 @@ export default defineComponent({
|
||||
this.activeScreen = Screen.Desk;
|
||||
await this.setDeskRoute();
|
||||
await fyo.telemetry.start(true);
|
||||
await checkForUpdates();
|
||||
await ipc.checkForUpdates();
|
||||
await setLanguageMap();
|
||||
this.dbPath = filePath;
|
||||
this.companyName = (await fyo.getValue(
|
||||
ModelNameEnum.AccountingSettings,
|
||||
@ -155,14 +157,12 @@ export default defineComponent({
|
||||
await this.setSearcher();
|
||||
updateConfigFiles(fyo);
|
||||
},
|
||||
async fileSelected(filePath: string, isNew?: boolean): Promise<void> {
|
||||
newDatabase() {
|
||||
this.activeScreen = Screen.SetupWizard;
|
||||
},
|
||||
async fileSelected(filePath: string): Promise<void> {
|
||||
fyo.config.set('lastSelectedFilePath', filePath);
|
||||
if (isNew) {
|
||||
this.activeScreen = Screen.SetupWizard;
|
||||
return;
|
||||
}
|
||||
|
||||
if (filePath !== ':memory:' && !(await checkDbAccess(filePath))) {
|
||||
if (filePath !== ':memory:' && !(await ipc.checkDbAccess(filePath))) {
|
||||
await showDialog({
|
||||
title: this.t`Cannot open file`,
|
||||
type: 'error',
|
||||
@ -182,12 +182,10 @@ export default defineComponent({
|
||||
}
|
||||
},
|
||||
async setupComplete(setupWizardOptions: SetupWizardOptions): Promise<void> {
|
||||
const filePath = fyo.config.get('lastSelectedFilePath');
|
||||
if (typeof filePath !== 'string') {
|
||||
return;
|
||||
}
|
||||
|
||||
const companyName = setupWizardOptions.companyName;
|
||||
const filePath = await ipc.getDbDefaultPath(companyName);
|
||||
await setupInstance(filePath, setupWizardOptions, fyo);
|
||||
fyo.config.set('lastSelectedFilePath', filePath);
|
||||
await this.setDesk(filePath);
|
||||
},
|
||||
async showSetupWizardOrDesk(filePath: string): Promise<void> {
|
||||
|
@ -57,7 +57,6 @@
|
||||
<script lang="ts">
|
||||
import { Field } from 'schemas/types';
|
||||
import { fyo } from 'src/initFyo';
|
||||
import { selectFile } from 'src/utils/ipcCalls';
|
||||
import { getDataURL } from 'src/utils/misc';
|
||||
import { defineComponent, PropType } from 'vue';
|
||||
import FeatherIcon from '../FeatherIcon.vue';
|
||||
@ -105,7 +104,7 @@ export default defineComponent({
|
||||
],
|
||||
};
|
||||
|
||||
const { name, success, data } = await selectFile(options);
|
||||
const { name, success, data } = await ipc.selectFile(options);
|
||||
|
||||
if (!success) {
|
||||
return;
|
||||
|
@ -75,8 +75,8 @@ export default {
|
||||
},
|
||||
async openNewDoc() {
|
||||
const schemaName = this.getTargetSchemaName();
|
||||
if(!schemaName){
|
||||
return
|
||||
if (!schemaName) {
|
||||
return;
|
||||
}
|
||||
const name =
|
||||
this.linkValue || fyo.doc.getTemporaryName(fyo.schemaMap[schemaName]);
|
||||
|
@ -87,6 +87,7 @@
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { t } from 'fyo';
|
||||
import { Verb } from 'fyo/telemetry/types';
|
||||
import { Field, FieldTypeEnum } from 'schemas/types';
|
||||
import { fyo } from 'src/initFyo';
|
||||
import {
|
||||
@ -95,16 +96,15 @@ import {
|
||||
getExportTableFields,
|
||||
getJsonExportData,
|
||||
} from 'src/utils/export';
|
||||
import { getSavePath, saveData, showExportInFolder } from 'src/utils/ipcCalls';
|
||||
import { ExportField, ExportFormat, ExportTableField } from 'src/utils/types';
|
||||
import { getSavePath, showExportInFolder } from 'src/utils/ui';
|
||||
import { QueryFilter } from 'utils/db/types';
|
||||
import { defineComponent, PropType } from 'vue';
|
||||
import { PropType, defineComponent } from 'vue';
|
||||
import Button from './Button.vue';
|
||||
import Check from './Controls/Check.vue';
|
||||
import Int from './Controls/Int.vue';
|
||||
import Select from './Controls/Select.vue';
|
||||
import FormHeader from './FormHeader.vue';
|
||||
import { Verb } from 'fyo/telemetry/types';
|
||||
|
||||
interface ExportWizardData {
|
||||
useListFilters: boolean;
|
||||
@ -257,7 +257,7 @@ export default defineComponent({
|
||||
return;
|
||||
}
|
||||
|
||||
await saveData(data, filePath);
|
||||
await ipc.saveData(data, filePath);
|
||||
this.fyo.telemetry.log(Verb.Exported, this.schemaName, {
|
||||
extension: this.exportFormat,
|
||||
});
|
||||
|
@ -9,7 +9,6 @@
|
||||
</button>
|
||||
</template>
|
||||
<script>
|
||||
import { openLink } from 'src/utils/ipcCalls';
|
||||
import FeatherIcon from './FeatherIcon.vue';
|
||||
|
||||
export default {
|
||||
@ -23,7 +22,7 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
openHelpLink() {
|
||||
openLink(this.link);
|
||||
ipc.openLink(this.link);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -197,13 +197,12 @@
|
||||
import { fyo } from 'src/initFyo';
|
||||
import { getBgTextColorClass } from 'src/utils/colors';
|
||||
import { searcherKey, shortcutsKey } from 'src/utils/injectionKeys';
|
||||
import { openLink } from 'src/utils/ipcCalls';
|
||||
import { docsPathMap } from 'src/utils/misc';
|
||||
import {
|
||||
getGroupLabelMap,
|
||||
SearchGroup,
|
||||
searchGroups,
|
||||
SearchItems,
|
||||
getGroupLabelMap,
|
||||
searchGroups,
|
||||
} from 'src/utils/search';
|
||||
import { defineComponent, inject, nextTick } from 'vue';
|
||||
import Button from './Button.vue';
|
||||
@ -305,7 +304,7 @@ export default defineComponent({
|
||||
},
|
||||
methods: {
|
||||
openDocs() {
|
||||
openLink('https://docs.frappebooks.com/' + docsPathMap.Search);
|
||||
ipc.openLink('https://docs.frappebooks.com/' + docsPathMap.Search);
|
||||
},
|
||||
getShortcuts() {
|
||||
const ifOpen = (cb: Function) => () => this.openModal && cb();
|
||||
|
@ -182,7 +182,6 @@
|
||||
import { reportIssue } from 'src/errorHandling';
|
||||
import { fyo } from 'src/initFyo';
|
||||
import { languageDirectionKey, shortcutsKey } from 'src/utils/injectionKeys';
|
||||
import { openLink } from 'src/utils/ipcCalls';
|
||||
import { docsPathRef } from 'src/utils/refs';
|
||||
import { getSidebarConfig } from 'src/utils/sidebarConfig';
|
||||
import { SidebarConfig, SidebarItem, SidebarRoot } from 'src/utils/types';
|
||||
@ -255,7 +254,7 @@ export default defineComponent({
|
||||
reportIssue,
|
||||
toggleSidebar,
|
||||
openDocumentation() {
|
||||
openLink('https://docs.frappebooks.com/' + docsPathRef.value);
|
||||
ipc.openLink('https://docs.frappebooks.com/' + docsPathRef.value);
|
||||
},
|
||||
setActiveGroup() {
|
||||
const { fullPath } = this.$router.currentRoute.value;
|
||||
|
@ -1,15 +1,13 @@
|
||||
import { t } from 'fyo';
|
||||
import { Doc } from 'fyo/model/doc';
|
||||
import type { Doc } from 'fyo/model/doc';
|
||||
import { BaseError } from 'fyo/utils/errors';
|
||||
import { ErrorLog } from 'fyo/utils/types';
|
||||
import { truncate } from 'lodash';
|
||||
import { showDialog } from 'src/utils/interactive';
|
||||
import { IPC_ACTIONS, IPC_MESSAGES } from 'utils/messages';
|
||||
import { fyo } from './initFyo';
|
||||
import router from './router';
|
||||
import { getErrorMessage, stringifyCircular } from './utils';
|
||||
import { DialogOptions, ToastOptions } from './utils/types';
|
||||
const { ipcRenderer } = require('electron');
|
||||
import type { DialogOptions, ToastOptions } from './utils/types';
|
||||
|
||||
function shouldNotStore(error: Error) {
|
||||
const shouldLog = (error as BaseError).shouldStore ?? true;
|
||||
@ -43,7 +41,7 @@ export async function sendError(errorLogObj: ErrorLog) {
|
||||
console.log('sendError', body);
|
||||
}
|
||||
|
||||
await ipcRenderer.invoke(IPC_ACTIONS.SEND_ERROR, JSON.stringify(body));
|
||||
await ipc.sendError(JSON.stringify(body));
|
||||
}
|
||||
|
||||
function getToastProps(errorLogObj: ErrorLog) {
|
||||
@ -119,7 +117,7 @@ export async function handleErrorWithDialog(
|
||||
};
|
||||
|
||||
if (reportError) {
|
||||
options.detail = truncate(options.detail, { length: 128 });
|
||||
options.detail = truncate(String(options.detail), { length: 128 });
|
||||
options.buttons = [
|
||||
{
|
||||
label: t`Report`,
|
||||
@ -154,8 +152,7 @@ export async function showErrorDialog(title?: string, content?: string) {
|
||||
// To be used for show stopper errors
|
||||
title ??= t`Error`;
|
||||
content ??= t`Something has gone terribly wrong. Please check the console and raise an issue.`;
|
||||
|
||||
await ipcRenderer.invoke(IPC_ACTIONS.SHOW_ERROR, { title, content });
|
||||
await ipc.showError(title, content);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
@ -232,7 +229,7 @@ function getIssueUrlQuery(errorLogObj?: ErrorLog): string {
|
||||
|
||||
export function reportIssue(errorLogObj?: ErrorLog) {
|
||||
const urlQuery = getIssueUrlQuery(errorLogObj);
|
||||
ipcRenderer.send(IPC_MESSAGES.OPEN_EXTERNAL, urlQuery);
|
||||
ipc.openExternalUrl(urlQuery);
|
||||
}
|
||||
|
||||
function getErrorLabel(error: Error) {
|
||||
|
@ -246,12 +246,10 @@ import Loading from 'src/components/Loading.vue';
|
||||
import Modal from 'src/components/Modal.vue';
|
||||
import { fyo } from 'src/initFyo';
|
||||
import { showDialog } from 'src/utils/interactive';
|
||||
import { deleteDb, getSavePath, getSelectedFilePath } from 'src/utils/ipcCalls';
|
||||
import { updateConfigFiles } from 'src/utils/misc';
|
||||
import { IPC_ACTIONS } from 'utils/messages';
|
||||
import { deleteDb, getSavePath, getSelectedFilePath } from 'src/utils/ui';
|
||||
import type { ConfigFilesWithModified } from 'utils/types';
|
||||
import { defineComponent } from 'vue';
|
||||
const { ipcRenderer } = require('electron');
|
||||
|
||||
export default defineComponent({
|
||||
name: 'DatabaseSelector',
|
||||
@ -262,7 +260,7 @@ export default defineComponent({
|
||||
Modal,
|
||||
Button,
|
||||
},
|
||||
emits: ['file-selected'],
|
||||
emits: ['file-selected', 'new-database'],
|
||||
data() {
|
||||
return {
|
||||
openModal: false,
|
||||
@ -360,25 +358,17 @@ export default defineComponent({
|
||||
this.creatingDemo = false;
|
||||
},
|
||||
async setFiles() {
|
||||
const dbList: ConfigFilesWithModified[] = await ipcRenderer.invoke(
|
||||
IPC_ACTIONS.GET_DB_LIST
|
||||
);
|
||||
|
||||
const dbList = await ipc.getDbList();
|
||||
this.files = dbList?.sort(
|
||||
(a, b) => Date.parse(b.modified) - Date.parse(a.modified)
|
||||
);
|
||||
},
|
||||
async newDatabase() {
|
||||
newDatabase() {
|
||||
if (this.creatingDemo) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { filePath, canceled } = await getSavePath('books', 'db');
|
||||
if (canceled || !filePath) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.emitFileSelected(filePath, true);
|
||||
this.$emit('new-database');
|
||||
},
|
||||
async existingDatabase() {
|
||||
if (this.creatingDemo) {
|
||||
@ -395,17 +385,12 @@ export default defineComponent({
|
||||
|
||||
this.emitFileSelected(file.dbPath);
|
||||
},
|
||||
emitFileSelected(filePath: string, isNew?: boolean) {
|
||||
emitFileSelected(filePath: string) {
|
||||
if (!filePath) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isNew) {
|
||||
this.$emit('file-selected', filePath, isNew);
|
||||
return;
|
||||
}
|
||||
|
||||
this.$emit('file-selected', filePath, !!isNew);
|
||||
this.$emit('file-selected', filePath);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -76,10 +76,8 @@ import Icon from 'src/components/Icon.vue';
|
||||
import PageHeader from 'src/components/PageHeader.vue';
|
||||
import { fyo } from 'src/initFyo';
|
||||
import { getGetStartedConfig } from 'src/utils/getStartedConfig';
|
||||
import { openLink } from 'src/utils/ipcCalls';
|
||||
import { GetStartedConfigItem } from 'src/utils/types';
|
||||
import { Component } from 'vue';
|
||||
import { defineComponent, h } from 'vue';
|
||||
import { Component, defineComponent, h } from 'vue';
|
||||
|
||||
type ListItem = GetStartedConfigItem['items'][number];
|
||||
|
||||
@ -96,9 +94,6 @@ export default defineComponent({
|
||||
sections: getGetStartedConfig(),
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
// this.sections = getGetStartedConfig();
|
||||
},
|
||||
async activated() {
|
||||
await fyo.doc.getDoc('GetStarted');
|
||||
await this.checkForCompletedTasks();
|
||||
@ -106,7 +101,7 @@ export default defineComponent({
|
||||
methods: {
|
||||
async handleDocumentation({ key, documentation }: ListItem) {
|
||||
if (documentation) {
|
||||
openLink(documentation);
|
||||
ipc.openLink(documentation);
|
||||
}
|
||||
|
||||
switch (key) {
|
||||
|
@ -388,10 +388,9 @@ import PageHeader from 'src/components/PageHeader.vue';
|
||||
import { Importer, TemplateField, getColumnLabel } from 'src/importer';
|
||||
import { fyo } from 'src/initFyo';
|
||||
import { showDialog } from 'src/utils/interactive';
|
||||
import { getSavePath, saveData } from 'src/utils/ipcCalls';
|
||||
import { docsPathMap } from 'src/utils/misc';
|
||||
import { docsPathRef } from 'src/utils/refs';
|
||||
import { selectTextFile } from 'src/utils/ui';
|
||||
import { getSavePath, selectTextFile } from 'src/utils/ui';
|
||||
import { defineComponent } from 'vue';
|
||||
import Loading from '../components/Loading.vue';
|
||||
|
||||
@ -795,7 +794,7 @@ export default defineComponent({
|
||||
return;
|
||||
}
|
||||
|
||||
await saveData(template, filePath);
|
||||
await ipc.saveData(template, filePath);
|
||||
},
|
||||
async preImportValidations(): Promise<boolean> {
|
||||
const title = this.t`Cannot Import`;
|
||||
|
@ -80,7 +80,6 @@ import { getErrorMessage } from 'src/utils';
|
||||
import { evaluateHidden } from 'src/utils/doc';
|
||||
import { shortcutsKey } from 'src/utils/injectionKeys';
|
||||
import { showDialog } from 'src/utils/interactive';
|
||||
import { reloadWindow } from 'src/utils/ipcCalls';
|
||||
import { docsPathMap } from 'src/utils/misc';
|
||||
import { docsPathRef } from 'src/utils/refs';
|
||||
import { UIGroupedFields } from 'src/utils/types';
|
||||
@ -222,7 +221,7 @@ export default defineComponent({
|
||||
{
|
||||
label: this.t`Yes`,
|
||||
isPrimary: true,
|
||||
action: reloadWindow,
|
||||
action: ipc.reloadWindow.bind(ipc),
|
||||
},
|
||||
{
|
||||
label: this.t`No`,
|
||||
|
@ -233,7 +233,6 @@ import ShortcutKeys from 'src/components/ShortcutKeys.vue';
|
||||
import { handleErrorWithDialog } from 'src/errorHandling';
|
||||
import { shortcutsKey } from 'src/utils/injectionKeys';
|
||||
import { showDialog, showToast } from 'src/utils/interactive';
|
||||
import { getSavePath } from 'src/utils/ipcCalls';
|
||||
import { docsPathMap } from 'src/utils/misc';
|
||||
import {
|
||||
PrintTemplateHint,
|
||||
@ -248,6 +247,7 @@ import {
|
||||
focusOrSelectFormControl,
|
||||
getActionsForDoc,
|
||||
getDocFromNameIfExistsElseNew,
|
||||
getSavePath,
|
||||
openSettings,
|
||||
selectTextFile,
|
||||
} from 'src/utils/ui';
|
||||
|
@ -1,17 +1,10 @@
|
||||
const { ipcRenderer } = require('electron');
|
||||
import { DateTime } from 'luxon';
|
||||
import { CUSTOM_EVENTS, IPC_ACTIONS } from 'utils/messages';
|
||||
import { CUSTOM_EVENTS } from 'utils/messages';
|
||||
import { UnexpectedLogObject } from 'utils/types';
|
||||
import { App as VueApp, createApp } from 'vue';
|
||||
import App from './App.vue';
|
||||
import Badge from './components/Badge.vue';
|
||||
import FeatherIcon from './components/FeatherIcon.vue';
|
||||
import {
|
||||
getErrorHandled,
|
||||
getErrorHandledSync,
|
||||
handleError,
|
||||
sendError,
|
||||
} from './errorHandling';
|
||||
import { handleError, sendError } from './errorHandling';
|
||||
import { fyo } from './initFyo';
|
||||
import { outsideClickDirective } from './renderer/helpers';
|
||||
import registerIpcRendererListeners from './renderer/registerIpcRendererListeners';
|
||||
@ -27,13 +20,8 @@ import { setLanguageMap } from './utils/language';
|
||||
}
|
||||
fyo.store.language = language || 'English';
|
||||
|
||||
ipcRenderer.send = getErrorHandledSync(ipcRenderer.send.bind(ipcRenderer));
|
||||
ipcRenderer.invoke = getErrorHandled(ipcRenderer.invoke.bind(ipcRenderer));
|
||||
|
||||
registerIpcRendererListeners();
|
||||
const { isDevelopment, platform, version } = (await ipcRenderer.invoke(
|
||||
IPC_ACTIONS.GET_ENV
|
||||
)) as { isDevelopment: boolean; platform: string; version: string };
|
||||
const { isDevelopment, platform, version } = await ipc.getEnv();
|
||||
|
||||
fyo.store.isDevelopment = isDevelopment;
|
||||
fyo.store.appVersion = version;
|
||||
@ -125,10 +113,6 @@ function setOnWindow(isDevelopment: boolean) {
|
||||
window.router = router;
|
||||
// @ts-ignore
|
||||
window.fyo = fyo;
|
||||
// @ts-ignore
|
||||
window.DateTime = DateTime;
|
||||
// @ts-ignore
|
||||
window.ipcRenderer = ipcRenderer;
|
||||
}
|
||||
|
||||
function getPlatformName(platform: string) {
|
||||
|
@ -1,11 +1,8 @@
|
||||
const { ipcRenderer } = require('electron');
|
||||
import { handleError } from 'src/errorHandling';
|
||||
import { fyo } from 'src/initFyo';
|
||||
import { IPC_CHANNELS } from 'utils/messages';
|
||||
|
||||
export default function registerIpcRendererListeners() {
|
||||
ipcRenderer.on(
|
||||
IPC_CHANNELS.LOG_MAIN_PROCESS_ERROR,
|
||||
ipc.registerMainProcessErrorListener(
|
||||
(_, error: unknown, more?: Record<string, unknown>) => {
|
||||
if (!(error instanceof Error)) {
|
||||
throw error;
|
||||
@ -27,7 +24,7 @@ export default function registerIpcRendererListeners() {
|
||||
}
|
||||
);
|
||||
|
||||
ipcRenderer.on(IPC_CHANNELS.CONSOLE_LOG, (_, ...stuff: unknown[]) => {
|
||||
ipc.registerConsoleLogListener((_, ...stuff: unknown[]) => {
|
||||
if (!fyo.store.isDevelopment) {
|
||||
return;
|
||||
}
|
||||
|
6
src/shims-tsx.d.ts
vendored
6
src/shims-tsx.d.ts
vendored
@ -1,6 +1,8 @@
|
||||
import type { IPC } from 'main/preload';
|
||||
import Vue, { VNode } from 'vue';
|
||||
|
||||
declare global {
|
||||
const ipc: IPC;
|
||||
namespace JSX {
|
||||
type Element = VNode;
|
||||
type ElementClass = Vue;
|
||||
@ -9,4 +11,8 @@ declare global {
|
||||
[elem: string]: any;
|
||||
}
|
||||
}
|
||||
|
||||
interface Window {
|
||||
ipc: IPC;
|
||||
}
|
||||
}
|
||||
|
@ -1,156 +0,0 @@
|
||||
/**
|
||||
* Utils that make ipcRenderer calls.
|
||||
*/
|
||||
const { ipcRenderer } = require('electron');
|
||||
import { t } from 'fyo';
|
||||
import { BaseError } from 'fyo/utils/errors';
|
||||
import type { BackendResponse } from 'utils/ipc/types';
|
||||
import { IPC_ACTIONS, IPC_MESSAGES } from 'utils/messages';
|
||||
import type {
|
||||
LanguageMap,
|
||||
SelectFileOptions,
|
||||
SelectFileReturn,
|
||||
TemplateFile,
|
||||
} from 'utils/types';
|
||||
import { showDialog, showToast } from './interactive';
|
||||
import { setLanguageMap } from './language';
|
||||
import type { OpenDialogReturnValue } from 'electron';
|
||||
|
||||
export function reloadWindow() {
|
||||
return ipcRenderer.send(IPC_MESSAGES.RELOAD_MAIN_WINDOW);
|
||||
}
|
||||
|
||||
export async function getLanguageMap(code: string) {
|
||||
return (await ipcRenderer.invoke(IPC_ACTIONS.GET_LANGUAGE_MAP, code)) as {
|
||||
languageMap: LanguageMap;
|
||||
success: boolean;
|
||||
message: string;
|
||||
};
|
||||
}
|
||||
|
||||
export async function getSelectedFilePath(): Promise<OpenDialogReturnValue> {
|
||||
return (await ipcRenderer.invoke(IPC_ACTIONS.GET_OPEN_FILEPATH, {
|
||||
title: t`Select file`,
|
||||
properties: ['openFile'],
|
||||
filters: [{ name: 'SQLite DB File', extensions: ['db'] }],
|
||||
})) as OpenDialogReturnValue;
|
||||
}
|
||||
|
||||
export async function getTemplates(): Promise<TemplateFile[]> {
|
||||
return (await ipcRenderer.invoke(
|
||||
IPC_ACTIONS.GET_TEMPLATES
|
||||
)) as TemplateFile[];
|
||||
}
|
||||
|
||||
export async function selectFile(
|
||||
options: SelectFileOptions
|
||||
): Promise<SelectFileReturn> {
|
||||
return (await ipcRenderer.invoke(
|
||||
IPC_ACTIONS.SELECT_FILE,
|
||||
options
|
||||
)) as SelectFileReturn;
|
||||
}
|
||||
|
||||
export async function checkDbAccess(filePath: string) {
|
||||
return (await ipcRenderer.invoke(
|
||||
IPC_ACTIONS.CHECK_DB_ACCESS,
|
||||
filePath
|
||||
)) as boolean;
|
||||
}
|
||||
|
||||
export async function checkForUpdates() {
|
||||
await ipcRenderer.invoke(IPC_ACTIONS.CHECK_FOR_UPDATES);
|
||||
await setLanguageMap();
|
||||
}
|
||||
|
||||
export function openLink(link: string) {
|
||||
ipcRenderer.send(IPC_MESSAGES.OPEN_EXTERNAL, link);
|
||||
}
|
||||
|
||||
export async function deleteDb(filePath: string) {
|
||||
const { error } = (await ipcRenderer.invoke(
|
||||
IPC_ACTIONS.DELETE_FILE,
|
||||
filePath
|
||||
)) as BackendResponse;
|
||||
|
||||
if (error?.code === 'EBUSY') {
|
||||
await showDialog({
|
||||
title: t`Delete Failed`,
|
||||
detail: t`Please restart and try again.`,
|
||||
type: 'error',
|
||||
});
|
||||
} else if (error?.code === 'ENOENT') {
|
||||
await showDialog({
|
||||
title: t`Delete Failed`,
|
||||
detail: t`File ${filePath} does not exist.`,
|
||||
type: 'error',
|
||||
});
|
||||
} else if (error?.code === 'EPERM') {
|
||||
await showDialog({
|
||||
title: t`Cannot Delete`,
|
||||
detail: t`Close Frappe Books and try manually.`,
|
||||
type: 'error',
|
||||
});
|
||||
} else if (error) {
|
||||
const err = new BaseError(500, error.message);
|
||||
err.name = error.name;
|
||||
err.stack = error.stack;
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
export async function saveData(data: string, savePath: string) {
|
||||
await ipcRenderer.invoke(IPC_ACTIONS.SAVE_DATA, data, savePath);
|
||||
}
|
||||
|
||||
export function showItemInFolder(filePath: string) {
|
||||
ipcRenderer.send(IPC_MESSAGES.SHOW_ITEM_IN_FOLDER, filePath);
|
||||
}
|
||||
|
||||
export async function makePDF(
|
||||
html: string,
|
||||
savePath: string,
|
||||
width: number,
|
||||
height: number
|
||||
): Promise<void> {
|
||||
const success = (await ipcRenderer.invoke(
|
||||
IPC_ACTIONS.SAVE_HTML_AS_PDF,
|
||||
html,
|
||||
savePath,
|
||||
width,
|
||||
height
|
||||
)) as boolean;
|
||||
|
||||
if (success) {
|
||||
showExportInFolder(t`Save as PDF Successful`, savePath);
|
||||
} else {
|
||||
showToast({ message: t`Export Failed`, type: 'error' });
|
||||
}
|
||||
}
|
||||
|
||||
export function showExportInFolder(message: string, filePath: string) {
|
||||
showToast({
|
||||
message,
|
||||
actionText: t`Open Folder`,
|
||||
type: 'success',
|
||||
action: () => {
|
||||
showItemInFolder(filePath);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function getSavePath(name: string, extention: string) {
|
||||
const response = (await ipcRenderer.invoke(IPC_ACTIONS.GET_SAVE_FILEPATH, {
|
||||
title: t`Select Folder`,
|
||||
defaultPath: `${name}.${extention}`,
|
||||
})) as { canceled: boolean; filePath?: string };
|
||||
|
||||
const canceled = response.canceled;
|
||||
let filePath = response.filePath;
|
||||
|
||||
if (filePath && !filePath.endsWith(extention) && filePath !== ':memory:') {
|
||||
filePath = `${filePath}.${extention}`;
|
||||
}
|
||||
|
||||
return { canceled, filePath };
|
||||
}
|
@ -1,7 +1,6 @@
|
||||
import { DEFAULT_LANGUAGE } from 'fyo/utils/consts';
|
||||
import { setLanguageMapOnTranslationString } from 'fyo/utils/translation';
|
||||
import { fyo } from 'src/initFyo';
|
||||
import { getLanguageMap, reloadWindow } from './ipcCalls';
|
||||
import { systemLanguageRef } from './refs';
|
||||
|
||||
// Language: Language Code in books/translations
|
||||
@ -45,7 +44,7 @@ export async function setLanguageMap(
|
||||
}
|
||||
|
||||
if (!dontReload && success && initLanguage !== oldLanguage) {
|
||||
reloadWindow();
|
||||
ipc.reloadWindow();
|
||||
}
|
||||
return success;
|
||||
}
|
||||
@ -63,7 +62,7 @@ function getLanguageCode(initLanguage: string, oldLanguage: string) {
|
||||
}
|
||||
|
||||
async function fetchAndSetLanguageMap(code: string) {
|
||||
const { success, message, languageMap } = await getLanguageMap(code);
|
||||
const { success, message, languageMap } = await ipc.getLanguageMap(code);
|
||||
|
||||
if (!success) {
|
||||
const { showToast } = await import('src/utils/interactive');
|
||||
|
@ -1,13 +1,17 @@
|
||||
import { Fyo } from 'fyo';
|
||||
import { Fyo, t } from 'fyo';
|
||||
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 { getValueMapFromList } from 'utils/index';
|
||||
import { TemplateFile } from 'utils/types';
|
||||
import { getSavePath, getTemplates, makePDF } from './ipcCalls';
|
||||
import { showToast } from './interactive';
|
||||
import { PrintValues } from './types';
|
||||
import { getDocFromNameIfExistsElseNew } from './ui';
|
||||
import {
|
||||
getDocFromNameIfExistsElseNew,
|
||||
getSavePath,
|
||||
showExportInFolder,
|
||||
} from './ui';
|
||||
|
||||
export type PrintTemplateHint = {
|
||||
[key: string]: string | PrintTemplateHint | PrintTemplateHint[];
|
||||
@ -223,13 +227,18 @@ export async function getPathAndMakePDF(
|
||||
width: number,
|
||||
height: number
|
||||
) {
|
||||
const { filePath } = await getSavePath(name, 'pdf');
|
||||
if (!filePath) {
|
||||
const { filePath: savePath } = await getSavePath(name, 'pdf');
|
||||
if (!savePath) {
|
||||
return;
|
||||
}
|
||||
|
||||
const html = constructPrintDocument(innerHTML);
|
||||
await makePDF(html, filePath, width, height);
|
||||
const success = await ipc.makePDF(html, savePath, width, height);
|
||||
if (success) {
|
||||
showExportInFolder(t`Save as PDF Successful`, savePath);
|
||||
} else {
|
||||
showToast({ message: t`Export Failed`, type: 'error' });
|
||||
}
|
||||
}
|
||||
|
||||
function constructPrintDocument(innerHTML: string) {
|
||||
@ -267,7 +276,7 @@ function getAllCSSAsStyleElem() {
|
||||
}
|
||||
|
||||
export async function updatePrintTemplates(fyo: Fyo) {
|
||||
const templateFiles = await getTemplates();
|
||||
const templateFiles = await ipc.getTemplates();
|
||||
const existingTemplates = (await fyo.db.getAll(ModelNameEnum.PrintTemplate, {
|
||||
fields: ['name', 'modified'],
|
||||
filters: { isCustom: false },
|
||||
|
@ -6,7 +6,12 @@ import { t } from 'fyo';
|
||||
import type { Doc } from 'fyo/model/doc';
|
||||
import { Action } from 'fyo/model/types';
|
||||
import { getActions } from 'fyo/utils';
|
||||
import { getDbError, LinkValidationError, ValueError } from 'fyo/utils/errors';
|
||||
import {
|
||||
BaseError,
|
||||
getDbError,
|
||||
LinkValidationError,
|
||||
ValueError,
|
||||
} from 'fyo/utils/errors';
|
||||
import { Invoice } from 'models/baseModels/Invoice/Invoice';
|
||||
import { PurchaseInvoice } from 'models/baseModels/PurchaseInvoice/PurchaseInvoice';
|
||||
import { SalesInvoice } from 'models/baseModels/SalesInvoice/SalesInvoice';
|
||||
@ -18,11 +23,11 @@ import { Schema } from 'schemas/types';
|
||||
import { handleErrorWithDialog } from 'src/errorHandling';
|
||||
import { fyo } from 'src/initFyo';
|
||||
import router from 'src/router';
|
||||
import { assertIsType } from 'utils/index';
|
||||
import { SelectFileOptions } from 'utils/types';
|
||||
import { RouteLocationRaw } from 'vue-router';
|
||||
import { evaluateHidden } from './doc';
|
||||
import { showDialog, showToast } from './interactive';
|
||||
import { selectFile } from './ipcCalls';
|
||||
import { showSidebar } from './refs';
|
||||
import {
|
||||
ActionGroup,
|
||||
@ -31,7 +36,6 @@ import {
|
||||
ToastOptions,
|
||||
UIGroupedFields,
|
||||
} from './types';
|
||||
import { assertIsType } from 'utils/index';
|
||||
|
||||
export const toastDurationMap = { short: 2_500, long: 5_000 } as const;
|
||||
|
||||
@ -434,7 +438,9 @@ export async function selectTextFile(filters?: SelectFileOptions['filters']) {
|
||||
title: t`Select File`,
|
||||
filters,
|
||||
};
|
||||
const { success, canceled, filePath, data, name } = await selectFile(options);
|
||||
const { success, canceled, filePath, data, name } = await ipc.selectFile(
|
||||
options
|
||||
);
|
||||
|
||||
if (canceled || !success) {
|
||||
showToast({
|
||||
@ -953,3 +959,67 @@ export const paperSizeMap: Record<
|
||||
height: -1,
|
||||
},
|
||||
};
|
||||
|
||||
export function showExportInFolder(message: string, filePath: string) {
|
||||
showToast({
|
||||
message,
|
||||
actionText: t`Open Folder`,
|
||||
type: 'success',
|
||||
action: () => {
|
||||
ipc.showItemInFolder(filePath);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function deleteDb(filePath: string) {
|
||||
const { error } = await ipc.deleteFile(filePath);
|
||||
|
||||
if (error?.code === 'EBUSY') {
|
||||
await showDialog({
|
||||
title: t`Delete Failed`,
|
||||
detail: t`Please restart and try again.`,
|
||||
type: 'error',
|
||||
});
|
||||
} else if (error?.code === 'ENOENT') {
|
||||
await showDialog({
|
||||
title: t`Delete Failed`,
|
||||
detail: t`File ${filePath} does not exist.`,
|
||||
type: 'error',
|
||||
});
|
||||
} else if (error?.code === 'EPERM') {
|
||||
await showDialog({
|
||||
title: t`Cannot Delete`,
|
||||
detail: t`Close Frappe Books and try manually.`,
|
||||
type: 'error',
|
||||
});
|
||||
} else if (error) {
|
||||
const err = new BaseError(500, error.message);
|
||||
err.name = error.name;
|
||||
err.stack = error.stack;
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getSelectedFilePath() {
|
||||
return ipc.getOpenFilePath({
|
||||
title: t`Select file`,
|
||||
properties: ['openFile'],
|
||||
filters: [{ name: 'SQLite DB File', extensions: ['db'] }],
|
||||
});
|
||||
}
|
||||
|
||||
export async function getSavePath(name: string, extention: string) {
|
||||
const response = await ipc.getSaveFilePath({
|
||||
title: t`Select folder`,
|
||||
defaultPath: `${name}.${extention}`,
|
||||
});
|
||||
|
||||
const canceled = response.canceled;
|
||||
let filePath = response.filePath;
|
||||
|
||||
if (filePath && !filePath.endsWith(extention) && filePath !== ':memory:') {
|
||||
filePath = `${filePath}.${extention}`;
|
||||
}
|
||||
|
||||
return { canceled, filePath };
|
||||
}
|
||||
|
@ -44,10 +44,6 @@ const appSourcePath = path.join(root, 'dist_electron', 'build', 'main.js');
|
||||
});
|
||||
|
||||
test('fill setup form', async (t) => {
|
||||
await electronApp.evaluate(({ dialog }, filePath) => {
|
||||
dialog.showSaveDialog = () =>
|
||||
Promise.resolve({ canceled: false, filePath });
|
||||
}, ':memory:');
|
||||
await window.getByTestId('create-new-file').click();
|
||||
await window.getByTestId('submit-button').waitFor();
|
||||
|
||||
|
@ -25,6 +25,7 @@ export enum IPC_ACTIONS {
|
||||
GET_DB_LIST = 'get-db-list',
|
||||
GET_TEMPLATES = 'get-templates',
|
||||
DELETE_FILE = 'delete-file',
|
||||
GET_DB_DEFAULT_PATH = 'get-db-default-path',
|
||||
// Database messages
|
||||
DB_CREATE = 'db-create',
|
||||
DB_CONNECT = 'db-connect',
|
||||
|
@ -585,6 +585,13 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/assert/-/assert-1.5.6.tgz#a8b5a94ce5fb8f4ba65fdc37fc9507609114189e"
|
||||
integrity sha512-Y7gDJiIqb9qKUHfBQYOWGngUpLORtirAVPuj/CWJrU2C6ZM4/y3XLwuwfGMF8s7QzW746LQZx23m0+1FSgjfug==
|
||||
|
||||
"@types/better-sqlite3@^7.6.4":
|
||||
version "7.6.4"
|
||||
resolved "https://registry.yarnpkg.com/@types/better-sqlite3/-/better-sqlite3-7.6.4.tgz#102462611e67aadf950d3ccca10292de91e6f35b"
|
||||
integrity sha512-dzrRZCYPXIXfSR1/surNbJ/grU3scTaygS0OMzjlGf71i9sc2fGyHPXXiXmEvNIoE0cGwsanEFMVJxPXmco9Eg==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/cacheable-request@^6.0.1":
|
||||
version "6.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/cacheable-request/-/cacheable-request-6.0.2.tgz#c324da0197de0a98a2312156536ae262429ff6b9"
|
||||
|
Loading…
Reference in New Issue
Block a user