From 44fce6f33e4c906059dc87465ada7b81cc5bea75 Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Fri, 23 Jun 2023 02:45:30 +0200 Subject: [PATCH 01/50] feat(e2e): WIP e2e onboarding process --- e2e/authentication.spec.ts | 20 ++++++++++++++ e2e/onboarding.spec.ts | 55 ++++++++++++++++++++++++++++++++++++++ package-lock.json | 14 ++++++++-- package.json | 9 ++++--- 4 files changed, 92 insertions(+), 6 deletions(-) create mode 100644 e2e/onboarding.spec.ts diff --git a/e2e/authentication.spec.ts b/e2e/authentication.spec.ts index 7495458b7..556704052 100644 --- a/e2e/authentication.spec.ts +++ b/e2e/authentication.spec.ts @@ -1,4 +1,5 @@ import { test, expect, Page } from '@playwright/test'; +import { faker } from '@faker-js/faker'; let authPage: Page; @@ -30,6 +31,7 @@ test.describe('authentication', () => { await authPage.getByRole('link', { name: 'Sign up' }).click(); await expect(authPage.url()).toContain('/auth/register'); }); + test('should the email or password is not correct.', async () => {}); }); test.describe('register', () => { @@ -52,6 +54,24 @@ test.describe('authentication', () => { 'Password is a required field' ); }); + test('should signup successfully.', async () => { + const form = authPage.locator('form'); + await form + .locator('input[name="first_name"]') + .fill(faker.person.firstName()); + await form + .locator('input[name="last_name"]') + .fill(faker.person.lastName()); + await form.locator('input[name="email"]').fill(faker.internet.email()); + await form + .locator('input[name="password"]') + .fill(faker.internet.password()); + + await authPage.getByRole('button', { name: 'Register' }).click(); + await expect(authPage.locator('h1')).toContainText( + 'Register a New Organization now!' + ); + }); }); test.describe('reset password', () => { diff --git a/e2e/onboarding.spec.ts b/e2e/onboarding.spec.ts new file mode 100644 index 000000000..e94fda9aa --- /dev/null +++ b/e2e/onboarding.spec.ts @@ -0,0 +1,55 @@ +import { test, expect, Page } from '@playwright/test'; +import { faker } from '@faker-js/faker'; + +let authPage: Page; + +test.describe('onboarding', () => { + test.beforeAll(async ({ browser }) => { + authPage = await browser.newPage(); + await authPage.goto('/auth/register'); + + const form = authPage.locator('form'); + await form + .locator('input[name="first_name"]') + .fill(faker.person.firstName()); + await form.locator('input[name="last_name"]').fill(faker.person.lastName()); + await form.locator('input[name="email"]').fill(faker.internet.email()); + await form + .locator('input[name="password"]') + .fill(faker.internet.password()); + + await authPage.getByRole('button', { name: 'Register' }).click(); + }); + + test.only('should validation catch all required fields', async () => { + const form = authPage.locator('form'); + + await authPage.getByRole('button', { name: 'Save & Continue' }).click(); + + await expect(form).toContainText('Organization name is a required field'); + await expect(form).toContainText('Location is a required field'); + await expect(form).toContainText('Base currency is a required field'); + await expect(form).toContainText('Fiscal year is a required field'); + await expect(form).toContainText('Time zone is a required field'); + }); + test('should sign-out when click on Signout link', () => {}); + + test('should onboarding process success', () => { + // await page.getByRole('textbox').click(); + // await page.getByRole('textbox').fill('sdafasdf'); + // await page.getByRole('button', { name: 'Select Business Location...' }).click(); + // await page.locator('a').filter({ hasText: 'Armenia' }).click(); + // await page.getByRole('button', { name: 'Select Base Currency...' }).click(); + // await page.locator('a').filter({ hasText: 'USD - US Dollar' }).click(); + // await page.getByRole('button', { name: 'Select Fiscal Year...' }).click(); + // await page.locator('a').filter({ hasText: 'May - April' }).click(); + // await page.getByRole('button', { name: 'Select Time Zone...' }).click(); + // await page.getByText('Pacific/Midway (SST)-11:00').click(); + // await page.getByRole('button', { name: 'Save & Continue' }).click(); + // await page.getByRole('heading', { name: 'Congrats! You are ready to go' }).click(); + // await page.getByRole('button', { name: 'Go to dashboard' }).click(); + }); + + + test('should go to the dashboard after clicking on "Go to dashboard" button.', () => {}); +}); diff --git a/package-lock.json b/package-lock.json index 8bdc5a647..b7080f99c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -333,6 +333,12 @@ "@jridgewell/trace-mapping": "0.3.9" } }, + "@faker-js/faker": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-8.0.2.tgz", + "integrity": "sha512-Uo3pGspElQW91PCvKSIAXoEgAUlRnH29sX2/p89kg7sP1m2PzCufHINd0FhTXQf6DYGiUlVncdSPa2F9wxed2A==", + "dev": true + }, "@gar/promisify": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", @@ -945,6 +951,7 @@ "version": "1.32.3", "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.32.3.tgz", "integrity": "sha512-BvWNvK0RfBriindxhLVabi8BRe3X0J9EVjKlcmhxjg4giWBD/xleLcg2dz7Tx0agu28rczjNIPQWznwzDwVsZQ==", + "dev": true, "requires": { "@types/node": "*", "fsevents": "2.3.2", @@ -954,7 +961,8 @@ "playwright-core": { "version": "1.32.3", "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.32.3.tgz", - "integrity": "sha512-SB+cdrnu74ZIn5Ogh/8278ngEh9NEEV0vR4sJFmK04h2iZpybfbqBY0bX6+BLYWVdV12JLLI+JEFtSnYgR+mWg==" + "integrity": "sha512-SB+cdrnu74ZIn5Ogh/8278ngEh9NEEV0vR4sJFmK04h2iZpybfbqBY0bX6+BLYWVdV12JLLI+JEFtSnYgR+mWg==", + "dev": true } } }, @@ -1003,7 +1011,8 @@ "@types/node": { "version": "18.14.6", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.14.6.tgz", - "integrity": "sha512-93+VvleD3mXwlLI/xASjw0FzKcwzl3OdTCzm1LaRfqgS21gfFtK3zDXM5Op9TeeMsJVOaJ2VRDpT9q4Y3d0AvA==" + "integrity": "sha512-93+VvleD3mXwlLI/xASjw0FzKcwzl3OdTCzm1LaRfqgS21gfFtK3zDXM5Op9TeeMsJVOaJ2VRDpT9q4Y3d0AvA==", + "dev": true }, "@types/normalize-package-data": { "version": "2.4.1", @@ -2324,6 +2333,7 @@ "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, "optional": true }, "function-bind": { diff --git a/package.json b/package.json index 167676cec..e8533a88e 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "dev:server": "lerna run dev --scope \"@bigcapital/server\"", "build:server": "lerna run build --scope \"@bigcapital/server\"", "serve:server": "lerna run serve --scope \"@bigcapital/server\"", - "test:e2e": "playwright test", + "test:e2e": "playwright test --debug", "prepare": "husky install" }, "workspaces": [ @@ -18,12 +18,13 @@ "shared/*" ], "devDependencies": { + "@commitlint/cli": "^17.4.2", "@commitlint/config-conventional": "^17.4.2", "@commitlint/config-lerna-scopes": "^17.4.2", + "@faker-js/faker": "^8.0.2", + "@playwright/test": "^1.32.3", "husky": "^8.0.3", - "lerna": "^6.4.1", - "@commitlint/cli": "^17.4.2", - "@playwright/test": "^1.32.3" + "lerna": "^6.4.1" }, "engines": { "node": "14.x" From b35d22d3b3722e2baaac1e1ae694e04f331e9194 Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Fri, 23 Jun 2023 14:32:36 +0200 Subject: [PATCH 02/50] feat(e2e): onboarding process --- e2e/_utils.ts | 6 ++ e2e/authentication.spec.ts | 56 +++++++++--- e2e/onboarding.spec.ts | 86 +++++++++++++------ packages/webapp/package-lock.json | 14 +-- packages/webapp/package.json | 4 +- .../DashboardTopbar/DashboardTopbar.tsx | 2 +- 6 files changed, 116 insertions(+), 52 deletions(-) create mode 100644 e2e/_utils.ts diff --git a/e2e/_utils.ts b/e2e/_utils.ts new file mode 100644 index 000000000..88858fc1f --- /dev/null +++ b/e2e/_utils.ts @@ -0,0 +1,6 @@ +import { Page } from "@playwright/test"; + + +export const clearLocalStorage = (page: Page) => { + return page.evaluate(`window.localStorage.clear()`); +} \ No newline at end of file diff --git a/e2e/authentication.spec.ts b/e2e/authentication.spec.ts index 556704052..0f4e72151 100644 --- a/e2e/authentication.spec.ts +++ b/e2e/authentication.spec.ts @@ -1,5 +1,6 @@ import { test, expect, Page } from '@playwright/test'; import { faker } from '@faker-js/faker'; +import { clearLocalStorage } from './_utils'; let authPage: Page; @@ -7,9 +8,16 @@ test.describe('authentication', () => { test.beforeAll(async ({ browser }) => { authPage = await browser.newPage(); }); + test.afterAll(async () => { + await authPage.close(); + }); + test.afterEach(async ({ context }) => { + context.clearCookies(); + await clearLocalStorage(authPage); + }); test.describe('login', () => { - test.beforeAll(async () => { + test.beforeEach(async () => { await authPage.goto('/auth/login'); }); test('should show the login page.', async () => { @@ -31,11 +39,23 @@ test.describe('authentication', () => { await authPage.getByRole('link', { name: 'Sign up' }).click(); await expect(authPage.url()).toContain('/auth/register'); }); - test('should the email or password is not correct.', async () => {}); + test('should the email or password is not correct.', async () => { + await authPage.getByLabel('Email Address').click(); + await authPage.getByLabel('Email Address').fill(faker.internet.email()); + + await authPage.getByLabel('Password').click(); + await authPage.getByLabel('Password').fill(faker.internet.password()); + + await authPage.getByRole('button', { name: 'Log in' }).click(); + + await expect(authPage.locator('body')).toContainText( + 'The email and password you entered did not match our records.' + ); + }); }); test.describe('register', () => { - test.beforeAll(async () => { + test.beforeEach(async () => { await authPage.goto('/auth/register'); }); test('should first name, last name, email and password be required.', async () => { @@ -56,18 +76,20 @@ test.describe('authentication', () => { }); test('should signup successfully.', async () => { const form = authPage.locator('form'); - await form - .locator('input[name="first_name"]') - .fill(faker.person.firstName()); - await form - .locator('input[name="last_name"]') - .fill(faker.person.lastName()); - await form.locator('input[name="email"]').fill(faker.internet.email()); - await form - .locator('input[name="password"]') - .fill(faker.internet.password()); + await form.getByLabel('First Name').click(); + await form.getByLabel('First Name').fill(faker.person.firstName()); + + await form.getByLabel('Email').click(); + await form.getByLabel('Email').fill(faker.internet.email()); + + await form.getByLabel('Last Name').click(); + await form.getByLabel('Last Name').fill(faker.person.lastName()); + + await form.getByLabel('Password').click(); + await form.getByLabel('Password').fill(faker.internet.password()); await authPage.getByRole('button', { name: 'Register' }).click(); + await expect(authPage.locator('h1')).toContainText( 'Register a New Organization now!' ); @@ -75,7 +97,13 @@ test.describe('authentication', () => { }); test.describe('reset password', () => { - test.beforeAll(async () => { + test.beforeAll(async ({ browser }) => { + authPage = await browser.newPage(); + }); + test.afterAll(async () => { + await authPage.close(); + }); + test.beforeEach(async () => { await authPage.goto('/auth/send_reset_password'); }); test('should email be required.', async () => { diff --git a/e2e/onboarding.spec.ts b/e2e/onboarding.spec.ts index e94fda9aa..1d25cd9bd 100644 --- a/e2e/onboarding.spec.ts +++ b/e2e/onboarding.spec.ts @@ -2,6 +2,7 @@ import { test, expect, Page } from '@playwright/test'; import { faker } from '@faker-js/faker'; let authPage: Page; +let businessLegalName: string = faker.company.name(); test.describe('onboarding', () => { test.beforeAll(async ({ browser }) => { @@ -9,19 +10,15 @@ test.describe('onboarding', () => { await authPage.goto('/auth/register'); const form = authPage.locator('form'); - await form - .locator('input[name="first_name"]') - .fill(faker.person.firstName()); - await form.locator('input[name="last_name"]').fill(faker.person.lastName()); - await form.locator('input[name="email"]').fill(faker.internet.email()); - await form - .locator('input[name="password"]') - .fill(faker.internet.password()); + + await form.getByLabel('First Name').fill(faker.person.firstName()); + await form.getByLabel('Email').fill(faker.internet.email()); + await form.getByLabel('Last Name').fill(faker.person.lastName()); + await form.getByLabel('Password').fill(faker.internet.password()); await authPage.getByRole('button', { name: 'Register' }).click(); }); - - test.only('should validation catch all required fields', async () => { + test('should validation catch all required fields', async () => { const form = authPage.locator('form'); await authPage.getByRole('button', { name: 'Save & Continue' }).click(); @@ -32,24 +29,57 @@ test.describe('onboarding', () => { await expect(form).toContainText('Fiscal year is a required field'); await expect(form).toContainText('Time zone is a required field'); }); - test('should sign-out when click on Signout link', () => {}); + test.describe('after onboarding', () => { + test.beforeAll(async () => { + await authPage.getByLabel('Legal Organization Name').click(); + await authPage + .getByLabel('Legal Organization Name') + .fill(businessLegalName); - test('should onboarding process success', () => { - // await page.getByRole('textbox').click(); - // await page.getByRole('textbox').fill('sdafasdf'); - // await page.getByRole('button', { name: 'Select Business Location...' }).click(); - // await page.locator('a').filter({ hasText: 'Armenia' }).click(); - // await page.getByRole('button', { name: 'Select Base Currency...' }).click(); - // await page.locator('a').filter({ hasText: 'USD - US Dollar' }).click(); - // await page.getByRole('button', { name: 'Select Fiscal Year...' }).click(); - // await page.locator('a').filter({ hasText: 'May - April' }).click(); - // await page.getByRole('button', { name: 'Select Time Zone...' }).click(); - // await page.getByText('Pacific/Midway (SST)-11:00').click(); - // await page.getByRole('button', { name: 'Save & Continue' }).click(); - // await page.getByRole('heading', { name: 'Congrats! You are ready to go' }).click(); - // await page.getByRole('button', { name: 'Go to dashboard' }).click(); + // Fill Business Location. + await authPage + .getByRole('button', { name: 'Select Business Location...' }) + .click(); + await authPage.locator('a').filter({ hasText: 'Albania' }).click(); + + // Fill Base Currency. + await authPage + .getByRole('button', { name: 'Select Base Currency...' }) + .click(); + await authPage + .locator('a') + .filter({ hasText: 'AED - United Arab Emirates Dirham' }) + .click(); + + // Fill Fasical Year. + await authPage + .getByRole('button', { name: 'Select Fiscal Year...' }) + .click(); + await authPage.locator('a').filter({ hasText: 'June - May' }).click(); + + // Fill Timezone. + await authPage + .getByRole('button', { name: 'Select Time Zone...' }) + .click(); + await authPage.getByText('Pacific/Marquesas-09:30').click(); + + // Click on Submit button + await authPage.getByRole('button', { name: 'Save & Continue' }).click(); + }); + test('should onboarding process success', async () => { + await expect(authPage.locator('body')).toContainText( + 'Congrats! You are ready to go', + { + timeout: 30000, + } + ); + }); + test('should go to the dashboard after clicking on "Go to dashboard" button.', async () => { + await authPage.getByRole('button', { name: 'Go to dashboard' }).click(); + + await expect( + authPage.locator('[data-testId="dashboard-topbar"] h1') + ).toContainText(businessLegalName); + }); }); - - - test('should go to the dashboard after clicking on "Go to dashboard" button.', () => {}); }); diff --git a/packages/webapp/package-lock.json b/packages/webapp/package-lock.json index b7d381a9e..0918dffb5 100644 --- a/packages/webapp/package-lock.json +++ b/packages/webapp/package-lock.json @@ -1,6 +1,6 @@ { "name": "@bigcapital/webapp", - "version": "1.7.1", + "version": "0.9.6", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -1205,9 +1205,9 @@ } }, "@blueprintjs-formik/core": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@blueprintjs-formik/core/-/core-0.3.3.tgz", - "integrity": "sha512-ko7g54YSEcSq2K/GEpmiTG0foGLqe7DwgXGhkGxYEiHhLAUv8WvQmrFsm8e/KOW7n8mLGq0uaZVe2l8m3JTGGQ==", + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@blueprintjs-formik/core/-/core-0.3.4.tgz", + "integrity": "sha512-gksuBYXXvX7IhZXbPEFEAHgmJWp1vt/GTMW0GYBkCoGBAvXy08hIHjMc85M0WNyen+tic3NRas6I2wsjgokgqw==", "requires": { "lodash.get": "^4.4.2", "lodash.keyby": "^4.6.0", @@ -1227,9 +1227,9 @@ } }, "@blueprintjs-formik/select": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@blueprintjs-formik/select/-/select-0.2.3.tgz", - "integrity": "sha512-j/zkX0B9wgtoHgK6Z/rlowB7F7zemrAajBU+d3caCoEYMMqwAI0XA++GytqrIhv5fEGjkZ1hkxS9j8eqX8vtjA==", + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/@blueprintjs-formik/select/-/select-0.2.5.tgz", + "integrity": "sha512-Sztf5dOemedUBfEjnDWD8ryfMU/x95hyhIgJT5/ywC/jQfX+d/K2OhujklTrCDzQilUeAJLoVkSdV+w77n8ckQ==", "requires": { "lodash.get": "^4.4.2", "lodash.keyby": "^4.6.0", diff --git a/packages/webapp/package.json b/packages/webapp/package.json index dc9228e83..96ac5d16f 100644 --- a/packages/webapp/package.json +++ b/packages/webapp/package.json @@ -3,9 +3,9 @@ "version": "0.9.6", "private": true, "dependencies": { - "@blueprintjs-formik/core": "^0.3.3", + "@blueprintjs-formik/core": "^0.3.4", "@blueprintjs-formik/datetime": "^0.3.4", - "@blueprintjs-formik/select": "^0.2.3", + "@blueprintjs-formik/select": "^0.2.5", "@blueprintjs/core": "^3.50.2", "@blueprintjs/datetime": "^3.23.12", "@blueprintjs/popover2": "^0.11.1", diff --git a/packages/webapp/src/components/Dashboard/DashboardTopbar/DashboardTopbar.tsx b/packages/webapp/src/components/Dashboard/DashboardTopbar/DashboardTopbar.tsx index c9e8e6946..549785a87 100644 --- a/packages/webapp/src/components/Dashboard/DashboardTopbar/DashboardTopbar.tsx +++ b/packages/webapp/src/components/Dashboard/DashboardTopbar/DashboardTopbar.tsx @@ -53,7 +53,7 @@ function DashboardTopbar({ }; return ( -
+
Date: Fri, 23 Jun 2023 16:10:29 +0200 Subject: [PATCH 03/50] chore: remove debug from playwright script --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e8533a88e..ebdcc185a 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "dev:server": "lerna run dev --scope \"@bigcapital/server\"", "build:server": "lerna run build --scope \"@bigcapital/server\"", "serve:server": "lerna run serve --scope \"@bigcapital/server\"", - "test:e2e": "playwright test --debug", + "test:e2e": "playwright test", "prepare": "husky install" }, "workspaces": [ From b46154ba59d0afdd7e983e5e78cb2b4cb138148c Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Tue, 27 Jun 2023 00:25:44 +0200 Subject: [PATCH 04/50] feat(e2e): add default extra header to new e2e browser pages --- e2e/_utils.ts | 13 ++++++++++--- e2e/authentication.spec.ts | 6 +++--- e2e/onboarding.spec.ts | 3 ++- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/e2e/_utils.ts b/e2e/_utils.ts index 88858fc1f..0d5dd8975 100644 --- a/e2e/_utils.ts +++ b/e2e/_utils.ts @@ -1,6 +1,13 @@ -import { Page } from "@playwright/test"; - +import { Page } from '@playwright/test'; export const clearLocalStorage = (page: Page) => { return page.evaluate(`window.localStorage.clear()`); -} \ No newline at end of file +}; + +export const defaultPageConfig = () => { + return { + extraHTTPHeaders: { + 'ngrok-skip-browser-warning': 'any-value', + }, + }; +}; diff --git a/e2e/authentication.spec.ts b/e2e/authentication.spec.ts index 0f4e72151..8e31e3ea1 100644 --- a/e2e/authentication.spec.ts +++ b/e2e/authentication.spec.ts @@ -1,12 +1,12 @@ import { test, expect, Page } from '@playwright/test'; import { faker } from '@faker-js/faker'; -import { clearLocalStorage } from './_utils'; +import { clearLocalStorage, defaultPageConfig } from './_utils'; let authPage: Page; test.describe('authentication', () => { test.beforeAll(async ({ browser }) => { - authPage = await browser.newPage(); + authPage = await browser.newPage({ ...defaultPageConfig() }); }); test.afterAll(async () => { await authPage.close(); @@ -98,7 +98,7 @@ test.describe('authentication', () => { test.describe('reset password', () => { test.beforeAll(async ({ browser }) => { - authPage = await browser.newPage(); + authPage = await browser.newPage({ ...defaultPageConfig() }); }); test.afterAll(async () => { await authPage.close(); diff --git a/e2e/onboarding.spec.ts b/e2e/onboarding.spec.ts index 1d25cd9bd..e80ae7f19 100644 --- a/e2e/onboarding.spec.ts +++ b/e2e/onboarding.spec.ts @@ -1,12 +1,13 @@ import { test, expect, Page } from '@playwright/test'; import { faker } from '@faker-js/faker'; +import { defaultPageConfig } from './_utils'; let authPage: Page; let businessLegalName: string = faker.company.name(); test.describe('onboarding', () => { test.beforeAll(async ({ browser }) => { - authPage = await browser.newPage(); + authPage = await browser.newPage({ ...defaultPageConfig() }); await authPage.goto('/auth/register'); const form = authPage.locator('form'); From 6373862044932020f821cb281203d990145cb5ba Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Tue, 27 Jun 2023 21:32:04 +0200 Subject: [PATCH 05/50] fix(webapp): No currency in amount field on money in/out dialogs --- packages/webapp/src/constants/sidebarMenu.tsx | 8 +- .../MoneyInDialog/MoneyInContentFields.tsx | 26 ++-- .../MoneyInDialog/MoneyInDialogContent.tsx | 1 - .../MoneyInDialog/MoneyInDialogProvider.tsx | 25 +-- .../MoneyInExchangeRateField.tsx | 26 ++++ .../MoneyInDialog/MoneyInFieldsProvider.tsx | 34 ++++ .../CashFlow/MoneyInDialog/MoneyInForm.tsx | 18 +-- .../MoneyInDialog/MoneyInFormFields.tsx | 10 +- .../OtherIncome/OtherIncomeFormFields.tsx | 126 +++++---------- .../OwnerContributionFormFields.tsx | 112 +++++--------- .../MoneyInDialog/TransactionTypeFields.tsx | 61 +++----- .../TransferFromAccountFormFields.tsx | 135 +++++----------- .../CashFlow/MoneyInDialog/index.tsx | 1 - .../CashFlow/MoneyInDialog/utils.tsx | 3 +- .../MoneyOutDialog/MoneyOutContentFields.tsx | 25 ++- .../MoneyOutDialog/MoneyOutDialogProvider.tsx | 28 ++-- .../MoneyOutExchangeRateField.tsx | 31 ++++ .../MoneyOutDialog/MoneyOutFieldsProvider.tsx | 34 ++++ .../MoneyOutFloatingActions.tsx | 10 +- .../CashFlow/MoneyOutDialog/MoneyOutForm.tsx | 16 +- .../MoneyOutDialog/MoneyOutFormDialog.tsx | 1 + .../MoneyOutDialog/MoneyOutFormFields.tsx | 14 +- .../OtherExpense/OtherExpnseFormFields.tsx | 137 +++++----------- .../OwnerDrawings/OwnerDrawingsFormFields.tsx | 126 +++++---------- .../MoneyOutDialog/TransactionTypeFields.tsx | 72 ++++----- .../TransferToAccountFormFields.tsx | 146 ++++++------------ .../CashFlow/MoneyOutDialog/utils.tsx | 3 +- .../CashFlow/CashflowTransactionForm.scss | 30 +--- 28 files changed, 519 insertions(+), 740 deletions(-) create mode 100644 packages/webapp/src/containers/CashFlow/MoneyInDialog/MoneyInExchangeRateField.tsx create mode 100644 packages/webapp/src/containers/CashFlow/MoneyInDialog/MoneyInFieldsProvider.tsx create mode 100644 packages/webapp/src/containers/CashFlow/MoneyOutDialog/MoneyOutExchangeRateField.tsx create mode 100644 packages/webapp/src/containers/CashFlow/MoneyOutDialog/MoneyOutFieldsProvider.tsx diff --git a/packages/webapp/src/constants/sidebarMenu.tsx b/packages/webapp/src/constants/sidebarMenu.tsx index 9a60691d2..3f5c16cb1 100644 --- a/packages/webapp/src/constants/sidebarMenu.tsx +++ b/packages/webapp/src/constants/sidebarMenu.tsx @@ -25,6 +25,7 @@ import { CashflowAction, PreferencesAbility, } from '@/constants/abilityOption'; +import { DialogsName } from './dialogs'; export const SidebarMenu = [ // --------------- @@ -114,7 +115,7 @@ export const SidebarMenu = [ text: , href: '/items/categories/new', type: ISidebarMenuItemType.Dialog, - dialogName: 'item-category-form', + dialogName: DialogsName.ItemCategoryForm, permission: { subject: AbilitySubject.Item, ability: ItemAction.Create, @@ -458,7 +459,7 @@ export const SidebarMenu = [ text: , href: '/cashflow-accounts', type: ISidebarMenuItemType.Dialog, - dialogName: 'money-in', + dialogName: DialogsName.MoneyInForm, permission: { subject: AbilitySubject.Cashflow, ability: CashflowAction.Create, @@ -468,6 +469,7 @@ export const SidebarMenu = [ text: , href: '/cashflow-accounts', type: ISidebarMenuItemType.Dialog, + dialogName: DialogsName.MoneyOutForm, permission: { subject: AbilitySubject.Cashflow, ability: CashflowAction.Create, @@ -477,6 +479,7 @@ export const SidebarMenu = [ text: , href: '/cashflow-accounts', type: ISidebarMenuItemType.Dialog, + dialogName: DialogsName.AccountForm, permission: { subject: AbilitySubject.Cashflow, ability: CashflowAction.Create, @@ -486,6 +489,7 @@ export const SidebarMenu = [ text: , href: '/cashflow-accounts', type: ISidebarMenuItemType.Dialog, + dialogName: DialogsName.AccountForm, permission: { subject: AbilitySubject.Cashflow, ability: CashflowAction.Create, diff --git a/packages/webapp/src/containers/CashFlow/MoneyInDialog/MoneyInContentFields.tsx b/packages/webapp/src/containers/CashFlow/MoneyInDialog/MoneyInContentFields.tsx index ee862b763..a5746c5f5 100644 --- a/packages/webapp/src/containers/CashFlow/MoneyInDialog/MoneyInContentFields.tsx +++ b/packages/webapp/src/containers/CashFlow/MoneyInDialog/MoneyInContentFields.tsx @@ -1,18 +1,22 @@ // @ts-nocheck -import React from 'react'; +import React, { useMemo } from 'react'; +import { useFormikContext } from 'formik'; import OwnerContributionFormFields from './OwnerContribution/OwnerContributionFormFields'; import OtherIncomeFormFields from './OtherIncome/OtherIncomeFormFields'; import TransferFromAccountFormFields from './TransferFromAccount/TransferFromAccountFormFields'; +import { MoneyInFieldsProvider } from './MoneyInFieldsProvider'; /** - * - * @param param0 - * @returns + * Money-in dialog content. + * Switches between fields based on the given transaction type. + * @returns {JSX.Element} */ -export default function MoneyInContentFields({ accountType }) { - const handleTransactionType = () => { - switch (accountType) { +export default function MoneyInContentFields() { + const { values } = useFormikContext(); + + const transactionFields = useMemo(() => { + switch (values.transaction_type) { case 'owner_contribution': return ; @@ -24,6 +28,10 @@ export default function MoneyInContentFields({ accountType }) { default: break; } - }; - return {handleTransactionType()}; + }, [values.transaction_type]); + + // Cannot continue if transaction type or account is not selected. + if (!values.transaction_type || !values.cashflow_account_id) return null; + + return {transactionFields}; } diff --git a/packages/webapp/src/containers/CashFlow/MoneyInDialog/MoneyInDialogContent.tsx b/packages/webapp/src/containers/CashFlow/MoneyInDialog/MoneyInDialogContent.tsx index 1674c4ab7..690d720ef 100644 --- a/packages/webapp/src/containers/CashFlow/MoneyInDialog/MoneyInDialogContent.tsx +++ b/packages/webapp/src/containers/CashFlow/MoneyInDialog/MoneyInDialogContent.tsx @@ -1,6 +1,5 @@ // @ts-nocheck import React from 'react'; - import { MoneyInDialogProvider } from './MoneyInDialogProvider'; import MoneyInForm from './MoneyInForm'; diff --git a/packages/webapp/src/containers/CashFlow/MoneyInDialog/MoneyInDialogProvider.tsx b/packages/webapp/src/containers/CashFlow/MoneyInDialog/MoneyInDialogProvider.tsx index 338bcf045..163f87119 100644 --- a/packages/webapp/src/containers/CashFlow/MoneyInDialog/MoneyInDialogProvider.tsx +++ b/packages/webapp/src/containers/CashFlow/MoneyInDialog/MoneyInDialogProvider.tsx @@ -1,11 +1,10 @@ // @ts-nocheck -import React from 'react'; +import React, { useState } from 'react'; import { DialogContent } from '@/components'; import { Features } from '@/constants'; import { useFeatureCan } from '@/hooks/state'; import { useCreateCashflowTransaction, - useAccount, useAccounts, useBranches, useCashflowAccounts, @@ -18,22 +17,21 @@ const MoneyInDialogContent = React.createContext(); * Money in dialog provider. */ function MoneyInDialogProvider({ - accountId, + accountId: defaultAccountId, accountType, dialogName, ...props }) { + // Holds the selected account id. + const [accountId, setAccountId] = useState(defaultAccountId); + + // Detarmines whether the feature is enabled. const { featureCan } = useFeatureCan(); const isBranchFeatureCan = featureCan(Features.Branches); // Fetches accounts list. const { isLoading: isAccountsLoading, data: accounts } = useAccounts(); - // Fetches the specific account details. - const { data: account, isLoading: isAccountLoading } = useAccount(accountId, { - enabled: !!accountId, - }); - // Fetches the branches list. const { data: branches, @@ -41,10 +39,11 @@ function MoneyInDialogProvider({ isSuccess: isBranchesSuccess, } = useBranches({}, { enabled: isBranchFeatureCan }); - // Fetch cash flow list . + // Fetch cash flow list. const { data: cashflowAccounts, isLoading: isCashFlowAccountsLoading } = useCashflowAccounts({}, { keepPreviousData: true }); + // Mutation create cashflow transaction. const { mutateAsync: createCashflowTransactionMutate } = useCreateCashflowTransaction(); @@ -57,9 +56,12 @@ function MoneyInDialogProvider({ // Provider data. const provider = { accounts, - account, branches, + accountId, + defaultAccountId, + setAccountId, + accountType, isAccountsLoading, isBranchesSuccess, @@ -77,8 +79,7 @@ function MoneyInDialogProvider({ isAccountsLoading || isCashFlowAccountsLoading || isBranchesLoading || - isSettingsLoading || - isAccountLoading; + isSettingsLoading; return ( diff --git a/packages/webapp/src/containers/CashFlow/MoneyInDialog/MoneyInExchangeRateField.tsx b/packages/webapp/src/containers/CashFlow/MoneyInDialog/MoneyInExchangeRateField.tsx new file mode 100644 index 000000000..9f5dc056b --- /dev/null +++ b/packages/webapp/src/containers/CashFlow/MoneyInDialog/MoneyInExchangeRateField.tsx @@ -0,0 +1,26 @@ +// @ts-nocheck +import React from 'react'; +import { ExchangeRateMutedField } from '@/components'; +import { useForeignAccount } from './utils'; +import { useFormikContext } from 'formik'; +import { useMoneyInFieldsContext } from './MoneyInFieldsProvider'; + +export function MoneyInExchangeRateField() { + const { account } = useMoneyInFieldsContext(); + const { values } = useFormikContext(); + + const isForeigAccount = useForeignAccount(); + + if (!isForeigAccount) return null; + + return ( + + ); +} diff --git a/packages/webapp/src/containers/CashFlow/MoneyInDialog/MoneyInFieldsProvider.tsx b/packages/webapp/src/containers/CashFlow/MoneyInDialog/MoneyInFieldsProvider.tsx new file mode 100644 index 000000000..22756d430 --- /dev/null +++ b/packages/webapp/src/containers/CashFlow/MoneyInDialog/MoneyInFieldsProvider.tsx @@ -0,0 +1,34 @@ +// @ts-nocheck +import React from 'react'; +import { DialogContent } from '@/components'; +import { useAccount } from '@/hooks/query'; +import { useMoneyInDailogContext } from './MoneyInDialogProvider'; + +const MoneyInFieldsContext = React.createContext(); + +/** + * Money in dialog provider. + */ +function MoneyInFieldsProvider({ ...props }) { + const { accountId } = useMoneyInDailogContext(); + + // Fetches the specific account details. + const { data: account, isLoading: isAccountLoading } = useAccount(accountId, { + enabled: !!accountId, + }); + // Provider data. + const provider = { + account, + }; + const isLoading = isAccountLoading; + + return ( + + + + ); +} + +const useMoneyInFieldsContext = () => React.useContext(MoneyInFieldsContext); + +export { MoneyInFieldsProvider, useMoneyInFieldsContext }; diff --git a/packages/webapp/src/containers/CashFlow/MoneyInDialog/MoneyInForm.tsx b/packages/webapp/src/containers/CashFlow/MoneyInDialog/MoneyInForm.tsx index e741e8ae5..9bdc1f9ed 100644 --- a/packages/webapp/src/containers/CashFlow/MoneyInDialog/MoneyInForm.tsx +++ b/packages/webapp/src/containers/CashFlow/MoneyInDialog/MoneyInForm.tsx @@ -53,7 +53,6 @@ function MoneyInForm({ accountId, accountType, createCashflowTransactionMutate, - submitPayload, } = useMoneyInDailogContext(); // transaction number. @@ -61,7 +60,6 @@ function MoneyInForm({ transactionNumberPrefix, transactionNextNumber, ); - // Initial form values. const initialValues = { ...defaultInitialValues, @@ -95,15 +93,13 @@ function MoneyInForm({ }; return ( -
- - - -
+ + + ); } diff --git a/packages/webapp/src/containers/CashFlow/MoneyInDialog/MoneyInFormFields.tsx b/packages/webapp/src/containers/CashFlow/MoneyInDialog/MoneyInFormFields.tsx index cd1e2a46d..ec6729dcc 100644 --- a/packages/webapp/src/containers/CashFlow/MoneyInDialog/MoneyInFormFields.tsx +++ b/packages/webapp/src/containers/CashFlow/MoneyInDialog/MoneyInFormFields.tsx @@ -12,17 +12,13 @@ import { useMoneyInDailogContext } from './MoneyInDialogProvider'; * Money in form fields. */ function MoneyInFormFields() { - const { values } = useFormikContext(); - // Money in dialog context. - const { accountId } = useMoneyInDailogContext(); + const { defaultAccountId } = useMoneyInDailogContext(); return (
- - - - + {!defaultAccountId && } +
); } diff --git a/packages/webapp/src/containers/CashFlow/MoneyInDialog/OtherIncome/OtherIncomeFormFields.tsx b/packages/webapp/src/containers/CashFlow/MoneyInDialog/OtherIncome/OtherIncomeFormFields.tsx index 06ccffea0..30ad76e9b 100644 --- a/packages/webapp/src/containers/CashFlow/MoneyInDialog/OtherIncome/OtherIncomeFormFields.tsx +++ b/packages/webapp/src/containers/CashFlow/MoneyInDialog/OtherIncome/OtherIncomeFormFields.tsx @@ -1,10 +1,9 @@ // @ts-nocheck import React from 'react'; -import { FastField, Field, ErrorMessage, useFormikContext } from 'formik'; +import { FastField, ErrorMessage } from 'formik'; import { Classes, FormGroup, - InputGroup, TextArea, Position, ControlGroup, @@ -18,14 +17,15 @@ import { FieldRequiredHint, Col, Row, - If, FeatureCan, BranchSelect, BranchSelectButton, - ExchangeRateMutedField, + FInputGroup, + FFormGroup, + FTextArea, + FMoneyInputGroup, } from '@/components'; import { DateInput } from '@blueprintjs/datetime'; -import { useAutofocus } from '@/hooks'; import { CLASSES, ACCOUNT_TYPE, Features } from '@/constants'; import { @@ -36,22 +36,18 @@ import { } from '@/utils'; import { useMoneyInDailogContext } from '../MoneyInDialogProvider'; -import { - useSetPrimaryBranchToForm, - useForeignAccount, - BranchRowDivider, -} from '../utils'; +import { useSetPrimaryBranchToForm, BranchRowDivider } from '../utils'; import { MoneyInOutTransactionNoField } from '../../_components'; +import { useMoneyInFieldsContext } from '../MoneyInFieldsProvider'; +import { MoneyInExchangeRateField } from '../MoneyInExchangeRateField'; /** * Other income form fields. */ export default function OtherIncomeFormFields() { // Money in dialog context. - const { accounts, account, branches } = useMoneyInDailogContext(); - const { values } = useFormikContext(); - const amountFieldRef = useAutofocus(); - const isForeigAccount = useForeignAccount(); + const { accounts, branches } = useMoneyInDailogContext(); + const { account } = useMoneyInFieldsContext(); // Sets the primary branch to form. useSetPrimaryBranchToForm(); @@ -61,17 +57,14 @@ export default function OtherIncomeFormFields() { - } - className={classNames('form-group--select-list', Classes.FILL)} - > + }> - + @@ -111,48 +104,26 @@ export default function OtherIncomeFormFields() { - {/*------------ amount -----------*/} - - {({ - form: { values, setFieldValue }, - field: { value }, - meta: { error, touched }, - }) => ( - + + } labelInfo={} - intent={inputIntent({ error, touched })} - helperText={} - className={'form-group--amount'} > - - { - setFieldValue('amount', amount); - }} - inputRef={(ref) => (amountFieldRef.current = ref)} - intent={inputIntent({ error, touched })} - /> + - - )} - + + + + + {/*------------ Exchange rate -----------*/} + - - {/*------------ exchange rate -----------*/} - - {/*------------ other income account -----------*/} @@ -182,43 +153,24 @@ export default function OtherIncomeFormFields() { )} + {/*------------ Reference -----------*/} - - {({ form, field, meta: { error, touched } }) => ( - } - intent={inputIntent({ error, touched })} - helperText={} - className={'form-group--reference-no'} - > - - - )} - + } name={'reference_no'}> + + - {/*------------ description -----------*/} - - {({ field, meta: { error, touched } }) => ( - } - className={'form-group--description'} - intent={inputIntent({ error, touched })} - helperText={} - > -