From 2a171a7d4f27da29c0f1891bd7fee0e79a7a0be1 Mon Sep 17 00:00:00 2001 From: Alaa Alsalehi Date: Sun, 10 May 2026 15:19:50 +0400 Subject: [PATCH 01/11] feat: implement demo dataset functionality and enhance dummy instance setup - Added support for fetching and listing demo datasets from the subscription server. - Introduced new types for demo dataset payloads and updated the setupDummyInstance function to handle demo data. - Enhanced the DatabaseSelector component to allow users to select demo datasets or use a built-in offline demo. - Refactored helper functions to accommodate new demo-related features and ensure proper state management during instance setup. - Updated configuration files to include demo metadata. This commit lays the groundwork for a more robust demo experience, enabling users to easily access sample data for testing and development. --- dummy/helpers.ts | 73 +++++-- dummy/index.ts | 288 +++++++++++++++++++------ dummy/types.ts | 63 ++++++ fyo/core/types.ts | 4 + main/demoData.ts | 100 +++++++++ main/helpers.ts | 8 +- main/preload.ts | 21 ++ main/registerIpcMainActionListeners.ts | 21 ++ main/subscription.ts | 6 +- src/pages/DatabaseSelector.vue | 148 +++++++++++-- src/utils/misc.ts | 29 ++- utils/messages.ts | 2 + utils/subscriptionFeatures.ts | 5 +- 13 files changed, 664 insertions(+), 104 deletions(-) create mode 100644 dummy/types.ts create mode 100644 main/demoData.ts diff --git a/dummy/helpers.ts b/dummy/helpers.ts index b9f7a0847..adb3277db 100644 --- a/dummy/helpers.ts +++ b/dummy/helpers.ts @@ -1,5 +1,9 @@ import { DateTime } from 'luxon'; +const DEFAULT_FLOW_VALUES = [ + 0.35, 0.25, 0.15, 0.15, 0.25, 0.05, 0.05, 0.15, 0.25, 0.35, 0.45, 0.55, +]; + // prettier-ignore export const partyPurchaseItemMap: Record = { 'Janky Office Spaces': ['Office Rent', 'Office Cleaning'], @@ -22,24 +26,61 @@ export const purchaseItemPartyMap: Record = Object.keys( return acc; }, {} as Record); -export const flow = [ - 0.35, // Jan - 0.25, // Feb - 0.15, // Mar - 0.15, // Apr - 0.25, // May - 0.05, // Jun - 0.05, // Jul - 0.15, // Aug - 0.25, // Sep - 0.35, // Oct - 0.45, // Nov - 0.55, // Dec -]; +/** @deprecated use getFlowArray() for dynamic demo payloads */ +export const flow = DEFAULT_FLOW_VALUES; + +let activeFlow: number[] = [...DEFAULT_FLOW_VALUES]; + +let activePurchaseItemPartyMap: Record = { + ...purchaseItemPartyMap, +}; + +function buildReversePurchaseMap( + map: Record +): Record { + const acc: Record = {}; + for (const party of Object.keys(map)) { + for (const item of map[party]) { + acc[item] = party; + } + } + return acc; +} + +export function resetDummyHelpers(): void { + activeFlow = [...DEFAULT_FLOW_VALUES]; + activePurchaseItemPartyMap = { ...purchaseItemPartyMap }; +} + +export function applyDummyHelpersOverrides( + flowOverride?: number[] | null, + partyPurchaseItemMapOverride?: Record | null +): void { + resetDummyHelpers(); + if (flowOverride && flowOverride.length === 12) { + activeFlow = [...flowOverride]; + } + if ( + partyPurchaseItemMapOverride && + Object.keys(partyPurchaseItemMapOverride).length + ) { + activePurchaseItemPartyMap = buildReversePurchaseMap( + partyPurchaseItemMapOverride + ); + } +} + +export function getFlowArray(): number[] { + return activeFlow; +} + +export function getPurchaseItemPartyMap(): Record { + return activePurchaseItemPartyMap; +} + export function getFlowConstant(months: number) { - // Jan to December const d = DateTime.now().minus({ months }); - return flow[d.month - 1]; + return activeFlow[d.month - 1]; } export function getRandomDates(count: number, months: number): Date[] { diff --git a/dummy/index.ts b/dummy/index.ts index 4ec21694a..10ac88316 100644 --- a/dummy/index.ts +++ b/dummy/index.ts @@ -11,60 +11,205 @@ import setupInstance from 'src/setup/setupInstance'; import { getMapFromList, safeParseInt } from 'utils'; import { getFiscalYear } from 'utils/misc'; import { - flow, + applyDummyHelpersOverrides, + getFlowArray, getFlowConstant, + getPurchaseItemPartyMap, getRandomDates, - purchaseItemPartyMap, + resetDummyHelpers, } from './helpers'; -import items from './items.json'; +import itemsCatalogDefault from './items.json'; import logo from './logo'; -import parties from './parties.json'; +import partiesCatalogDefault from './parties.json'; +import type { DemoDatasetPayload } from './types'; + +export type { DemoDatasetPayload } from './types'; type Notifier = (stage: string, percent: number) => void; +type CatalogItem = typeof itemsCatalogDefault[number]; +type CatalogParty = typeof partiesCatalogDefault[number]; + +const DEFAULT_PERIODIC_PURCHASES: Record = { + 'Marketing - Video': 2, + 'Social Ads': 1, + Electricity: 1, + 'Office Cleaning': 1, + 'Office Rent': 1, +}; + +let catalogItems: CatalogItem[] = itemsCatalogDefault; +let catalogParties: CatalogParty[] = partiesCatalogDefault; +let activePayload: DemoDatasetPayload | null = null; + +async function defaultReceivableAccount(fyo: Fyo): Promise { + if (await fyo.db.exists(ModelNameEnum.Account, 'Debtors')) { + return 'Debtors'; + } + if (await fyo.db.exists(ModelNameEnum.Account, 'Trade Receivable')) { + return 'Trade Receivable'; + } + return 'Debtors'; +} + +async function defaultPayableAccount(fyo: Fyo): Promise { + if (await fyo.db.exists(ModelNameEnum.Account, 'Creditors')) { + return 'Creditors'; + } + if (await fyo.db.exists(ModelNameEnum.Account, 'Trade Payable')) { + return 'Trade Payable'; + } + return 'Creditors'; +} + +async function defaultCashAccount(fyo: Fyo): Promise { + if (await fyo.db.exists(ModelNameEnum.Account, 'Cash')) { + return 'Cash'; + } + const rows = (await fyo.db.getAll(ModelNameEnum.Account, { + fields: ['name'], + filters: { accountType: 'Cash', isGroup: false }, + limit: 1, + })) as { name: string }[]; + return rows[0]?.name ?? 'Cash'; +} + +async function ensureUnitedArabEmiratesDemoTaxes(fyo: Fyo) { + const specs = [ + { name: 'VAT-5', rate: 5 }, + { name: 'VAT-0', rate: 0 }, + ]; + for (const spec of specs) { + if (await fyo.db.exists(ModelNameEnum.Tax, spec.name)) { + continue; + } + const doc = fyo.doc.getNewDoc( + ModelNameEnum.Tax, + { + name: spec.name, + details: [{ account: 'Sales Tax Payable', rate: spec.rate }], + }, + false + ); + await doc.sync(); + } +} + export async function setupDummyInstance( dbPath: string, fyo: Fyo, years = 1, baseCount = 1000, - notifier?: Notifier + notifier?: Notifier, + payload?: DemoDatasetPayload | null ) { + activePayload = payload ?? null; + catalogItems = (payload?.items as CatalogItem[]) ?? itemsCatalogDefault; + catalogParties = + (payload?.parties as CatalogParty[]) ?? partiesCatalogDefault; + applyDummyHelpersOverrides(payload?.flow, payload?.partyPurchaseItemMap); + await fyo.purgeCache(); notifier?.(fyo.t`Setting Up Instance`, -1); - const options = { - logo: null, - companyName: "Flo's Clothes", - country: 'India', - fullname: 'Lin Florentine', - email: 'lin@flosclothes.com', - bankName: 'Supreme Bank', - currency: 'INR', - fiscalYearStart: getFiscalYear('04-01', true)!.toISOString(), - fiscalYearEnd: getFiscalYear('04-01', false)!.toISOString(), - chartOfAccounts: 'India - Chart of Accounts', - }; - await setupInstance(dbPath, options, fyo); - fyo.store.skipTelemetryLogging = true; - - years = Math.floor(years); - notifier?.(fyo.t`Creating Items and Parties`, -1); - await generateStaticEntries(fyo); - await generateDynamicEntries(fyo, years, baseCount, notifier); - await setOtherSettings(fyo); - - const instanceId = (await fyo.getValue( - ModelNameEnum.SystemSettings, - 'instanceId' - )) as string; - await fyo.singles.SystemSettings?.setAndSync('hideGetStarted', true); - - fyo.store.skipTelemetryLogging = false; - return { companyName: options.companyName, instanceId }; + + const fyStart = payload?.options.fiscalYearStartMD ?? '04-01'; + const fyEnd = payload?.options.fiscalYearEndMD ?? '04-01'; + + const options = payload + ? { + logo: null as string | null, + companyName: payload.options.companyName, + country: payload.options.country, + fullname: payload.options.fullname ?? '', + email: payload.options.email ?? '', + bankName: payload.options.bankName ?? '', + currency: payload.options.currency, + fiscalYearStart: getFiscalYear(fyStart, true)!.toISOString(), + fiscalYearEnd: getFiscalYear(fyEnd, false)!.toISOString(), + chartOfAccounts: payload.options.chartOfAccounts, + } + : { + logo: null as string | null, + companyName: "Flo's Clothes", + country: 'India', + fullname: 'Lin Florentine', + email: 'lin@flosclothes.com', + bankName: 'Supreme Bank', + currency: 'INR', + fiscalYearStart: getFiscalYear('04-01', true)!.toISOString(), + fiscalYearEnd: getFiscalYear('04-01', false)!.toISOString(), + chartOfAccounts: 'India - Chart of Accounts', + }; + + try { + await setupInstance(dbPath, options, fyo); + if (payload?.options.country === 'United Arab Emirates') { + await ensureUnitedArabEmiratesDemoTaxes(fyo); + } + fyo.store.skipTelemetryLogging = true; + + years = Math.floor(years); + notifier?.(fyo.t`Creating Items and Parties`, -1); + await generateStaticEntries(fyo); + await generateDynamicEntries(fyo, years, baseCount, notifier); + await setOtherSettings(fyo, payload ?? undefined); + + const instanceId = (await fyo.getValue( + ModelNameEnum.SystemSettings, + 'instanceId' + )) as string; + await fyo.singles.SystemSettings?.setAndSync('hideGetStarted', true); + + fyo.store.skipTelemetryLogging = false; + return { companyName: options.companyName, instanceId }; + } finally { + activePayload = null; + catalogItems = itemsCatalogDefault; + catalogParties = partiesCatalogDefault; + resetDummyHelpers(); + } } -async function setOtherSettings(fyo: Fyo) { +async function setOtherSettings(fyo: Fyo, payload?: DemoDatasetPayload) { const doc = await fyo.doc.getDoc(ModelNameEnum.PrintSettings); const address = fyo.doc.getNewDoc(ModelNameEnum.Address); + + if (payload?.address) { + const emirateOrState = payload.address.state || payload.address.city || ''; + await address.setAndSync({ + addressLine1: payload.address.addressLine1 ?? '', + city: payload.address.city ?? '', + state: payload.address.state ?? '', + pos: emirateOrState, + postalCode: payload.address.postalCode ?? '', + country: payload.address.country ?? payload.options.country, + }); + + const ps = payload.printSettings; + const displayLogo = + typeof ps.displayLogo === 'boolean' + ? ps.displayLogo + : Boolean(ps.displayLogo); + await doc.setAndSync({ + color: ps.color ?? '#F687B3', + template: 'Business', + displayLogo, + phone: payload.accounting.phone ?? '', + logo, + address: address.name, + }); + + const acc = await fyo.doc.getDoc(ModelNameEnum.AccountingSettings); + const patch: Record = {}; + if (payload.options.country === 'India' && payload.accounting.taxId) { + patch.gstin = payload.accounting.taxId; + } + if (Object.keys(patch).length) { + await acc.setAndSync(patch); + } + return; + } + await address.setAndSync({ addressLine1: '1st Column, Fitzgerald Bridge', city: 'Pune', @@ -118,6 +263,9 @@ async function generateDynamicEntries( } async function getJournalEntries(fyo: Fyo, salesInvoices: SalesInvoice[]) { + if (activePayload?.options.country === 'United Arab Emirates') { + return []; + } const entries = []; const amount = salesInvoices .map((i) => i.items!) @@ -177,6 +325,9 @@ async function getJournalEntries(fyo: Fyo, salesInvoices: SalesInvoice[]) { } async function getPayments(fyo: Fyo, invoices: Invoice[]) { + const recvAccount = await defaultReceivableAccount(fyo); + const payAccount = await defaultPayableAccount(fyo); + const cashAccount = await defaultCashAccount(fyo); const payments = []; for (const invoice of invoices) { // Defaulters @@ -192,11 +343,11 @@ async function getPayments(fyo: Fyo, invoices: Invoice[]) { .plus({ hours: 1 }) .toJSDate(); if (doc.paymentType === 'Receive') { - doc.account = 'Debtors'; - doc.paymentAccount = 'Cash'; + doc.account = recvAccount; + doc.paymentAccount = cashAccount; } else { - doc.account = 'Cash'; - doc.paymentAccount = 'Creditors'; + doc.account = cashAccount; + doc.paymentAccount = payAccount; } doc.amount = invoice.outstandingAmount; @@ -239,8 +390,9 @@ async function getSalesInvoices( notifier?: Notifier ) { const invoices: SalesInvoice[] = []; - const salesItems = items.filter((i) => i.for !== 'Purchases'); - const customers = parties.filter((i) => i.role !== 'Supplier'); + const recvAccount = await defaultReceivableAccount(fyo); + const salesItems = catalogItems.filter((i) => i.for !== 'Purchases'); + const customers = catalogParties.filter((i) => i.role !== 'Supplier'); /** * Get certain number of entries for each month of the count @@ -271,7 +423,7 @@ async function getSalesInvoices( await doc.set('party', customer!.name); if (!doc.account) { - doc.account = 'Debtors'; + doc.account = recvAccount; } /** * Add `numItems` number of items to the invoice. @@ -296,7 +448,7 @@ async function getSalesInvoices( quantity = Math.ceil(Math.random() * 3); } - let fc = flow[date.getMonth()]; + let fc = getFlowArray()[date.getMonth()]; if (baseCount < 500) { fc += 1; } @@ -336,6 +488,7 @@ async function getSalesPurchaseInvoices( salesInvoices: SalesInvoice[] ): Promise { const invoices = [] as PurchaseInvoice[]; + const payAccount = await defaultPayableAccount(fyo); /** * Group all sales invoices by their YYYY-MM. */ @@ -395,7 +548,10 @@ async function getSalesPurchaseInvoices( }); const supplierGrouped = Object.keys(itemGrouped).reduce((acc, item) => { - const supplier = purchaseItemPartyMap[item]; + const supplier = getPurchaseItemPartyMap()[item]; + if (!supplier) { + return acc; + } acc[supplier] ??= []; acc[supplier].push(item); @@ -416,7 +572,7 @@ async function getSalesPurchaseInvoices( await doc.set('party', supplier); if (!doc.account) { - doc.account = 'Creditors'; + doc.account = payAccount; } /** @@ -439,15 +595,11 @@ async function getNonSalesPurchaseInvoices( fyo: Fyo, years: number ): Promise { - const purchaseItems = items.filter((i) => i.for !== 'Sales'); + const payAccount = await defaultPayableAccount(fyo); + const purchaseItems = catalogItems.filter((i) => i.for !== 'Sales'); const itemMap = getMapFromList(purchaseItems, 'name'); - const periodic: Record = { - 'Marketing - Video': 2, - 'Social Ads': 1, - Electricity: 1, - 'Office Cleaning': 1, - 'Office Rent': 1, - }; + const periodic = + activePayload?.periodicPurchases ?? DEFAULT_PERIODIC_PURCHASES; const invoices: SalesInvoice[] = []; for (const months of range(0, years * 12)) { @@ -462,6 +614,16 @@ async function getNonSalesPurchaseInvoices( continue; } + const party = getPurchaseItemPartyMap()[name]; + if (!party) { + continue; + } + + const item = itemMap[name]; + if (!item) { + continue; + } + const doc = fyo.doc.getNewDoc( ModelNameEnum.PurchaseInvoice, { @@ -470,20 +632,22 @@ async function getNonSalesPurchaseInvoices( false ) as PurchaseInvoice; - const party = purchaseItemPartyMap[name]; await doc.set('party', party); if (!doc.account) { - doc.account = 'Creditors'; + doc.account = payAccount; } await doc.append('items', {}); const row = doc.items!.at(-1)!; - const item = itemMap[name]; let quantity = 1; let rate = item.rate; - if (name === 'Social Ads') { + const rentLike = + name === 'Office Rent' || + name.includes('إيجار') || + (typeof name === 'string' && name.toLowerCase().includes('rent')); + if (item.rate < 120 && !rentLike) { quantity = Math.ceil(Math.random() * 200); - } else if (name !== 'Office Rent') { + } else if (!rentLike) { rate = rate * (Math.random() * 0.4 + 0.8); } @@ -506,15 +670,15 @@ async function generateStaticEntries(fyo: Fyo) { } async function generateItems(fyo: Fyo) { - for (const item of items) { - const doc = fyo.doc.getNewDoc('Item', item, false); + for (const raw of catalogItems) { + const doc = fyo.doc.getNewDoc('Item', raw, false); await doc.sync(); } } async function generateParties(fyo: Fyo) { - for (const party of parties) { - const doc = fyo.doc.getNewDoc('Party', party, false); + for (const raw of catalogParties) { + const doc = fyo.doc.getNewDoc('Party', raw, false); await doc.sync(); } } diff --git a/dummy/types.ts b/dummy/types.ts new file mode 100644 index 000000000..d7cf6fa05 --- /dev/null +++ b/dummy/types.ts @@ -0,0 +1,63 @@ +/** + * Payload returned by rukn_books_subscription.api.get_demo_dataset + * and consumed by setupDummyInstance. + */ +export type DemoDatasetPayload = { + key: string; + options: { + companyName: string; + fullname?: string; + email?: string; + bankName?: string; + country: string; + currency: string; + chartOfAccounts: string; + fiscalYearStartMD: string; + fiscalYearEndMD: string; + }; + address: { + addressLine1?: string; + city?: string; + state?: string; + postalCode?: string; + country?: string; + }; + accounting: { + taxId?: string; + phone?: string; + }; + printSettings: { + color?: string; + displayLogo?: number | boolean; + }; + items: DemoItemSeed[]; + parties: DemoPartySeed[]; + partyPurchaseItemMap: Record; + flow: number[]; + periodicPurchases?: Record; +}; + +export type DemoItemSeed = { + name: string; + description?: string | null; + unit: string; + itemType: string; + incomeAccount: string; + expenseAccount: string; + tax?: string; + rate: number; + hsnCode?: string | null; + for: string; + image?: string; +}; + +export type DemoPartySeed = { + name: string; + role: string; + defaultAccount: string; + currency: string; + email?: string | null; + phone?: string | null; + gstType?: string | null; + gstin?: string | null; +}; diff --git a/fyo/core/types.ts b/fyo/core/types.ts index e67ba07fb..1c401b744 100644 --- a/fyo/core/types.ts +++ b/fyo/core/types.ts @@ -83,6 +83,10 @@ export interface ConfigFile { companyName: string; dbPath: string; openCount: number; + /** True when this file was created via Create Demo (not New Company / ERPNext import). */ + isDemo?: boolean; + /** Demo persona key from subscription server, or flo-clothes for built-in offline demo. */ + demoKey?: string; } export interface FyoConfig { diff --git a/main/demoData.ts b/main/demoData.ts new file mode 100644 index 000000000..b7dde8a6e --- /dev/null +++ b/main/demoData.ts @@ -0,0 +1,100 @@ +import fetch from 'node-fetch'; +import type { DemoDatasetPayload } from 'dummy/types'; +import { + getErrorMessageFromResponse, + SUBSCRIPTION_SERVER, +} from './subscription'; + +export type DemoDatasetListRow = { + name: string; + key: string; + title_en?: string; + title_ar?: string; + description_en?: string; + description_ar?: string; + locale?: string; + industry?: string; + country?: string; + currency?: string; + preview_images?: string[]; +}; + +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null; +} + +export async function listDemoDatasets(token: string): Promise<{ + success: boolean; + message: string; + datasets: DemoDatasetListRow[]; +}> { + try { + const res = await fetch( + `${SUBSCRIPTION_SERVER}/api/method/rukn_books_subscription.api.list_demo_datasets`, + { + method: 'POST', + headers: { + Authorization: `token ${token}`, + }, + } + ); + if (res.status === 200) { + const body = (await res.json()) as { message?: unknown }; + const msg = body.message; + if (isRecord(msg) && Array.isArray(msg.datasets)) { + return { + success: true, + message: 'OK', + datasets: msg.datasets as DemoDatasetListRow[], + }; + } + return { success: true, message: 'OK', datasets: [] }; + } + return { + success: false, + message: await getErrorMessageFromResponse(res), + datasets: [], + }; + } catch (err) { + const m = err instanceof Error ? err.message : String(err); + return { success: false, message: m, datasets: [] }; + } +} + +export async function getDemoDataset( + token: string, + key: string +): Promise<{ + success: boolean; + message: string; + payload?: DemoDatasetPayload; +}> { + try { + const res = await fetch( + `${SUBSCRIPTION_SERVER}/api/method/rukn_books_subscription.api.get_demo_dataset?key=${encodeURIComponent( + key + )}`, + { + method: 'POST', + headers: { + Authorization: `token ${token}`, + }, + } + ); + if (res.status === 200) { + const body = (await res.json()) as { message?: unknown }; + return { + success: true, + message: 'OK', + payload: body.message as DemoDatasetPayload, + }; + } + return { + success: false, + message: await getErrorMessageFromResponse(res), + }; + } catch (err) { + const m = err instanceof Error ? err.message : String(err); + return { success: false, message: m }; + } +} diff --git a/main/helpers.ts b/main/helpers.ts index 3ec78849b..979bab905 100644 --- a/main/helpers.ts +++ b/main/helpers.ts @@ -36,14 +36,12 @@ export async function setAndGetCleanedConfigFiles() { export async function getConfigFilesWithModified(files: ConfigFile[]) { const filesWithModified: ConfigFilesWithModified[] = []; - for (const { dbPath, id, companyName, openCount } of files) { + for (const file of files) { + const { dbPath } = file; const { mtime } = await fs.stat(dbPath); filesWithModified.push({ - id, - dbPath, - companyName, + ...file, modified: mtime.toISOString(), - openCount, }); } diff --git a/main/preload.ts b/main/preload.ts index ee7d215c8..6e3b4e096 100644 --- a/main/preload.ts +++ b/main/preload.ts @@ -359,6 +359,27 @@ const ipc = { }; }, + async listDemoDatasets() { + return (await ipcRenderer.invoke( + IPC_ACTIONS.LIST_DEMO_DATASETS + )) as { + success: boolean; + message: string; + datasets: Array>; + }; + }, + + async getDemoDataset(key: string) { + return (await ipcRenderer.invoke( + IPC_ACTIONS.GET_DEMO_DATASET, + key + )) as { + success: boolean; + message: string; + payload?: Record; + }; + }, + registerMainProcessErrorListener(listener: IPCRendererListener) { ipcRenderer.on(IPC_CHANNELS.LOG_MAIN_PROCESS_ERROR, listener); }, diff --git a/main/registerIpcMainActionListeners.ts b/main/registerIpcMainActionListeners.ts index 47620f23a..391f75ba4 100644 --- a/main/registerIpcMainActionListeners.ts +++ b/main/registerIpcMainActionListeners.ts @@ -41,6 +41,7 @@ import verifyTokenWithServer, { syncDatabaseToServer, reportIssueToServer, } from './subscription'; +import { getDemoDataset, listDemoDatasets } from './demoData'; function sanitizeFilename(name: string) { return name @@ -464,6 +465,26 @@ export default function registerIpcMainActionListeners(main: Main) { } ); + ipcMain.handle(IPC_ACTIONS.LIST_DEMO_DATASETS, async () => { + const token = retrieveToken(); + if (!token) { + return { + success: false, + message: 'No subscription token', + datasets: [] as unknown[], + }; + } + return await listDemoDatasets(token); + }); + + ipcMain.handle(IPC_ACTIONS.GET_DEMO_DATASET, async (_, key: string) => { + const token = retrieveToken(); + if (!token) { + return { success: false, message: 'No subscription token' }; + } + return await getDemoDataset(token, key); + }); + /** * Database Related Actions */ diff --git a/main/subscription.ts b/main/subscription.ts index 6cf1bb79d..edc81b4e7 100644 --- a/main/subscription.ts +++ b/main/subscription.ts @@ -5,7 +5,7 @@ import type { Response } from 'node-fetch'; import { randomBytes } from 'crypto'; import { SUBSCRIPTION_FEATURE_SERVER_MAP } from 'utils/subscriptionFeatures'; -const SUBSCRIPTION_SERVER = +export const SUBSCRIPTION_SERVER = process.env.NODE_ENV === 'development' ? 'http://localhost:8001' : 'https://books.rukn.sh'; @@ -15,7 +15,9 @@ function isRecord(value: unknown): value is Record { return typeof value === 'object' && value !== null; } -async function getErrorMessageFromResponse(res: Response): Promise { +export async function getErrorMessageFromResponse( + res: Response +): Promise { try { const raw = await res.text(); const t = raw.trim(); diff --git a/src/pages/DatabaseSelector.vue b/src/pages/DatabaseSelector.vue index 9a750bf01..59ee8a8ca 100644 --- a/src/pages/DatabaseSelector.vue +++ b/src/pages/DatabaseSelector.vue @@ -26,17 +26,19 @@

{{ - t`Create a new company or select an existing one from your computer` + canShowDemo && filesLoaded && !files?.length + ? t`Use Create Demo below to start with sample data. Saved companies appear in the list.` + : t`Create a new company or select an existing one from your computer` }}


- +
+ +
+
+ +
+ + +
@@ -574,6 +620,15 @@