diff --git a/.github/workflows/cypress_integration.yml b/.github/workflows/cypress_integration.yml index 92233ed2..8320f050 100644 --- a/.github/workflows/cypress_integration.yml +++ b/.github/workflows/cypress_integration.yml @@ -42,3 +42,11 @@ jobs: run: yarn build - name: Cypress run integration tests run: yarn test:integration:cypress + - name: cypress-integration-upload-artifacts + uses: actions/upload-artifact@v3 + if: failure() + with: + name: cypress-integration-artifacts + path: | + packages/cli/cypress/screenshots + packages/cli/cypress/videos diff --git a/.gitignore b/.gitignore index 3c991a8a..61e9734d 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ dev-app tmp-testMailQueue.json dist mailing.config.json +!packages/web/mailing.config.json previews_html out .mailing @@ -113,6 +114,7 @@ typings/ emails !packages/cli/src/generator_templates/ts/emails/ !packages/cli/src/generator_templates/js/emails/ +!packages/web/emails # E2E tests e2e/runs diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index e6f3bdf8..b6fd9f25 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -45,11 +45,11 @@ For development, you may want to have a demo next app that pulls in your changes ### Run embedded jest tests for the preview server -During the smoke test process described below, the jest tests in `scripts/e2e_test/jest_tests` are copied into the directory where each target framework is installed and run. Before testing them in the framework install context, however, you will want to make sure they pass on the latest build by running `yarn build` and then `yarn e2e:jest` in the mailing project root. +During the framework test process described below, the jest tests in `scripts/e2e_test/jest_tests` are copied into the directory where each target framework is installed and run. Before testing them in the framework install context, however, you will want to make sure they pass on the latest build by running `yarn build` and then `yarn e2e:jest` in the mailing project root. ## Smoke tests -The directory `scripts/e2e_test` contains smoke tests targeting supported frameworks that should be run before every public npm release. Each test uses the `yarn create` command to create new projects from their `create-*` starter kits and then runs the cypress cli tests contained in `packages/cli/cypress` and the jest tests contained in `scripts/e2e_test/jest_tests`. +The directory `scripts/e2e_test` contains framework tests targeting supported frameworks that should be run before every public npm release. Each test uses the `yarn create` command to create new projects from their `create-*` starter kits and then runs the cypress cli tests contained in `packages/cli/cypress` and the jest tests contained in `scripts/e2e_test/jest_tests`. The frameworks currently covered by the tests are: @@ -60,25 +60,28 @@ The frameworks currently covered by the tests are: - redwood_ts (Redwood with Typescript) - redwood_js (Redwood without Typescript) - remix_ts (Remix with Typescript) -- remid_js (Remix without Typescript) +- remix_js (Remix without Typescript) **Initial test setup** - In the project root, run `asdf install` to install ruby 3.1.2 - In the directory `scripts/e2e_test`, run `bundle install` to install the required ruby gems -**Run the smoke tests** +**Run the framework tests** -- In the project root, run `yarn e2e` to run the full smoke test suite, including cypress and jest tests. +- In the project root, run `yarn test:e2e` to run the full framework test suite, including cypress and jest tests for all supported frameworks. This will instantiate each framework, add mailing with yalc, and then run the cypress tests contained in `packages/cli/cypress` and the jest tests contained in `scripts/e2e_test/jest_tests`. -The script supports some options for running: +**Run the framework tests with advanced options** -- `--only=redwood_ts` to run the tests only on the specified framework. See TestRunner::E2E_CONFIG for a list of frameworks that are currently supported. +The underlying ruby script `bundle exec ruby e2e/cli.rb` supports some options for running: + +- `--app=redwood_ts` to run the tests only on the specified framework. See `e2e/app.rb` for the list of supported frameworks - `--skip-build` to skip the yarn build part of the script, useful when debugging something unrelated to the build +- `--update-snapshot` if you need to update the snapshots in the framework tests. This will run jest with the `-u` option and then copy the updated snapshots back to mailing. - `--rerun` to skip the framework install part of the script, useful when debugging something in your cypress tests unrelated to the build or the framework install. This will use the framework installs that are present in the runs/latest directory, i.e. the assumption is you've run a test against some framework(s) and you now want to re-running them after adjusting your cypress tests. **Cache the framework installs for faster runs** -- Use the `--save-cache` flag to save each framework install (before mailing is added) to the `cache` directory. Subsequent test runs will use the cache instead of running `yarn create` and `yarn install`, which will speed things up 🏎 If you need to reset the cache, e.g. if you want to test a newer version of the framework or if the framework install process changes, you can delete the cache directory or the subdirectory containing the specific framework you are targeting. +- Use the `--save-cache` flag to save each framework install (before mailing is added) to the `cache` directory. Subsequent test runs will start with a copy of the cache instead of running `yarn create` and `yarn install`, which will speed things up 🏎 If you need to reset the cache, e.g. if you want to test a newer version of the framework or if the framework install process changes, you can delete the cache directory or the subdirectory containing the specific framework you are targeting. diff --git a/package.json b/package.json index 842e5918..b2f87f19 100644 --- a/package.json +++ b/package.json @@ -21,14 +21,15 @@ "test": "yarn link:emails; yarn jest", "test:integration": "yarn integration:servers:start \"yarn link:emails; yarn jest -c jest.integration.config.ts $TEST_FLAGS\"", "test:integration:watch": "yarn integration:servers:start \"yarn link:emails; yarn jest -c jest.integration.config.ts --watch\"", - "test:integration:cypress": "yarn integration:servers:start \"cd packages/cli && yarn cypress run -C cypressIntegration.config.ts\"", - "test:integration:cypress:headed": "yarn integration:servers:start \"cd packages/cli && yarn cypress run -C cypressIntegration.config.ts --headed\"", - "ci:test": "yarn integration:servers:start test", + "ci:test": "yarn test:servers:start test", "ci:test:integration": "yarn integration:servers:start \"yarn link:emails; yarn jest -c jest.integration.config.ts\"", + "test:integration:cypress": "yarn integration:servers:start \"cd packages/cli && yarn cypress run -C cypressIntegration.config.ts\"", + "test:integration:cypress:open": "yarn integration:servers:start \"cd packages/cli && yarn cypress open -C cypressIntegration.config.ts\"", "ci:cli": "cd packages/cli && yarn ci:server", "ci:web": "cd packages/web && yarn ci:server", "test:e2e": "bundle && bundle exec ruby e2e/cli.rb run-all", - "integration:servers:start": "./scripts/assert-free-ports.sh && MAILING_CI=true MAILING_DATABASE_URL=$MAILING_DATABASE_URL_TEST WEB_DATABASE_URL=$WEB_DATABASE_URL_TEST start-test ci:cli :3883 ci:web :3000", + "test:servers:start": "./scripts/assert-free-ports.sh && MAILING_API_KEY=testApiKey MAILING_API_URL=http://localhost:3883 MAILING_DATABASE_URL=$MAILING_DATABASE_URL_TEST WEB_DATABASE_URL=$WEB_DATABASE_URL_TEST start-test ci:cli :3883 ci:web :3000", + "integration:servers:start": "MAILING_INTEGRATION_TEST=true yarn test:servers:start", "prepublish": "yarn build", "postinstall": "husky install && preconstruct dev", "watch": "preconstruct watch", diff --git a/packages/cli/cypress/e2e/__integration__/audience.cy.integration.ts b/packages/cli/cypress/e2e/__integration__/audience.cy.integration.ts new file mode 100644 index 00000000..7731c463 --- /dev/null +++ b/packages/cli/cypress/e2e/__integration__/audience.cy.integration.ts @@ -0,0 +1,20 @@ +describe("audience", () => { + before(() => { + cy.task("db:reset"); + }); + + it("should show subscribers on the /audiences page", () => { + cy.signup(); + + // subscribe a user to the default list + cy.visit("/settings"); + cy.get("a").contains("Subscribe").click(); + cy.get("input#email").type("ok@ok.com"); + cy.get("button[type=submit]").click(); + cy.get("body").should("contain", "Thanks for subscribing!"); + + // ok@ok.com should appear on the /audiences page + cy.visit("/audiences"); + cy.get(".table-data").should("contain", "ok@ok.com"); + }); +}); diff --git a/packages/cli/cypress/e2e/__integration__/signupAndAuthenticate.cy.integration.js b/packages/cli/cypress/e2e/__integration__/signupAndAuthenticate.cy.integration.ts similarity index 82% rename from packages/cli/cypress/e2e/__integration__/signupAndAuthenticate.cy.integration.js rename to packages/cli/cypress/e2e/__integration__/signupAndAuthenticate.cy.integration.ts index aa8dce13..3cb6eaee 100644 --- a/packages/cli/cypress/e2e/__integration__/signupAndAuthenticate.cy.integration.js +++ b/packages/cli/cypress/e2e/__integration__/signupAndAuthenticate.cy.integration.ts @@ -1,10 +1,12 @@ -describe("login tests", () => { +describe("signup and authenticate", () => { + const email = "test@mailing.run"; + const password = "password"; + before(() => { cy.task("db:reset"); }); it("should be able to signup, login, and everything else", () => { - const email = "test@mailing.run"; cy.visit("/signup"); cy.location("pathname").should("eq", "/signup"); @@ -13,12 +15,12 @@ describe("login tests", () => { // invalid email should give an error cy.get("input#email").type("test"); - cy.get("form").submit(); + cy.get("button[type=submit]").click(); cy.get(".form-error").should("contain", "email is invalid"); // invalid password (blank) should give an error cy.get("input#email").clear().type(email); - cy.get("form").submit(); + cy.get("button[type=submit]").click(); cy.get(".form-error").should( "contain", "password should be at least 8 characters" @@ -26,33 +28,24 @@ describe("login tests", () => { // fill in email and passord fields with valid values and then submit the form cy.get("input#email").clear().type(email); - cy.get("input#password").type("password"); - cy.get("form").submit(); - - // it should redirect to the login page - cy.location("pathname").should("eq", "/login"); - cy.get("h1").should("contain", "Log in"); - - // fill in email and passord fields and then submit the form - cy.get("input#email").type(email); - cy.get("input#password").type("password"); - cy.get("form").submit(); + cy.get("input#password").type(password); + cy.get("button[type=submit]").click(); // it should redirect you to the settings page cy.location("pathname").should("eq", "/settings"); - cy.get("h1").should("contain", "Settings"); + cy.get("h1").should("contain", "Account"); // you should see a default api key that was created - cy.get("#api-keys tbody tr").should("have.length", 1); + cy.get("#api-keys .table-data").should("have.length", 3); // you should see a button to add an API key cy.get("button").should("contain", "New API Key"); // click the button to add an API key - cy.get("button").click(); + cy.get("button").contains("New API Key").click(); // you should see 2 api keys in the tbody instead of 1 - cy.get("#api-keys tbody tr").should("have.length", 2); + cy.get("#api-keys .table-data").should("have.length", 6); // you should get a 404 if you try to go back to /signup, only 1 user is allowed to signup cy.visit("/signup", { failOnStatusCode: false }); @@ -90,7 +83,7 @@ describe("login tests", () => { // it should give you an error if you try to login with the wrong password cy.get("input#email").type(email); cy.get("input#password").type("wrongpassword"); - cy.get("form").submit(); + cy.get("button[type=submit]").click(); cy.get(".form-error").should("contain", "invalid password"); }); @@ -104,12 +97,12 @@ describe("login tests", () => { // fill in email and passord fields and then submit the form cy.get("input#email").type("i@didnsignup.com"); cy.get("input#password").type("password"); - cy.get("form").submit(); + cy.get("button[type=submit]").click(); // it should show an error message cy.get("div.form-error").should( "contain", - "no user exists with that email" + "No user exists with that email." ); }); @@ -123,5 +116,14 @@ describe("login tests", () => { expect(response.status).to.eq(307); expect(response.redirectedToUrl).to.eq("http://localhost:3883/login"); }); + + // it should redirect to the login page + cy.visit("/login"); + cy.get("h1").should("contain", "Log in"); + + // fill in email and passord fields and then submit the form + cy.get("input#email").type(email); + cy.get("input#password").type(password); + cy.get("button[type=submit]").click(); }); }); diff --git a/packages/cli/cypress/e2e/__integration__/unsubscribe.cy.integration.ts b/packages/cli/cypress/e2e/__integration__/unsubscribe.cy.integration.ts new file mode 100644 index 00000000..e97a45df --- /dev/null +++ b/packages/cli/cypress/e2e/__integration__/unsubscribe.cy.integration.ts @@ -0,0 +1,62 @@ +describe("unsubscribe page", () => { + before(() => { + cy.task("db:reset"); + }); + + it("should not show the nav", async () => { + cy.signup(); + + // get the defalt list id from the database + cy.request({ + url: `/api/lists`, + }).then((response) => { + expect(response.status).to.eq(200); + + const listId = response.body.lists[0].id; + expect(listId).to.be.a("string"); + + // subscribe a user to the default list + cy.request({ + url: `/api/lists/${listId}/subscribe`, + method: "POST", + body: { + email: "test@test.com", + }, + }).then((response) => { + expect(response.status).to.eq(201); + const { + member: { id: memberId }, + } = response.body; + + expect(memberId).to.be.a("string"); + + // unsubscribe the user + cy.visit(`/unsubscribe/${memberId}`); + cy.get("h1").should("contain", "Email preferences"); + cy.get("nav").should("not.exist"); + + // check that the user is not unsubscribed + cy.get("label").should("have.length", 1); + cy.get("label").should("contain", "Unsubscribe from all emails"); + cy.get("input[type=checkbox]").should("have.length", 1); + cy.get("input[type=checkbox]").should("not.be.checked"); + + // check the unsubscribe all checkbox + cy.get("input[type=checkbox]").check(); + // click the submit button + cy.get("button").click(); + + // should see "Saved!" message + cy.get("div").should("contain", "Saved!"); + + // the user's status should be unsubscribed + cy.request({ + url: `/api/lists/${listId}/members/${memberId}`, + }).then((response) => { + expect(response.status).to.eq(200); + expect(response.body.member.status).to.eq("unsubscribed"); + }); + }); + }); + }); +}); diff --git a/packages/cli/cypress/support/commands.ts b/packages/cli/cypress/support/commands.ts index 95857aea..d2918c5a 100644 --- a/packages/cli/cypress/support/commands.ts +++ b/packages/cli/cypress/support/commands.ts @@ -25,13 +25,23 @@ // -- This will overwrite an existing command -- // Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) // -// declare global { -// namespace Cypress { -// interface Chainable { -// login(email: string, password: string): Chainable -// drag(subject: string, options?: Partial): Chainable -// dismiss(subject: string, options?: Partial): Chainable -// visit(originalFn: CommandOriginalFn, url: string, options: Partial): Chainable -// } -// } -// } + +/* eslint-disable-next-line @typescript-eslint/no-namespace */ +declare namespace Cypress { + interface Chainable { + signup(): Chainable; + } +} + +Cypress.Commands.add("signup", () => { + const email = "test123@mailing.run"; + cy.visit("/signup"); + cy.get("h1").should("contain", "Sign up"); + cy.location("pathname").should("eq", "/signup"); + + cy.get("input#email").type(email); + cy.get("input#password").type("password"); + cy.get("button[type=submit]").click(); + + cy.location("pathname").should("eq", "/settings"); +}); diff --git a/packages/cli/cypressIntegration.config.ts b/packages/cli/cypressIntegration.config.ts index a91172f7..e05c2265 100644 --- a/packages/cli/cypressIntegration.config.ts +++ b/packages/cli/cypressIntegration.config.ts @@ -13,6 +13,10 @@ export default defineConfig({ specPattern: "**/*.cy.integration.{js,jsx,ts,tsx}", defaultCommandTimeout: 10000, baseUrl: "http://localhost:3883", + retries: { + runMode: 2, + openMode: 0, + }, setupNodeEvents(on, _config) { on("task", { "db:reset": resetDb, diff --git a/packages/cli/prisma/migrations/20221028184445_add_is_default_to_lists/migration.sql b/packages/cli/prisma/migrations/20221028184445_add_is_default_to_lists/migration.sql new file mode 100644 index 00000000..38a346c4 --- /dev/null +++ b/packages/cli/prisma/migrations/20221028184445_add_is_default_to_lists/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "List" ADD COLUMN "isDefault" BOOLEAN NOT NULL DEFAULT false; diff --git a/packages/cli/prisma/migrations/20221104153624_member_and_list_timestamps/migration.sql b/packages/cli/prisma/migrations/20221104153624_member_and_list_timestamps/migration.sql new file mode 100644 index 00000000..08c0a9ad --- /dev/null +++ b/packages/cli/prisma/migrations/20221104153624_member_and_list_timestamps/migration.sql @@ -0,0 +1,14 @@ +/* + Warnings: + + - Added the required column `updatedAt` to the `List` table without a default value. This is not possible if the table is not empty. + - Added the required column `updatedAt` to the `Member` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "List" ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, +ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL; + +-- AlterTable +ALTER TABLE "Member" ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, +ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL; diff --git a/packages/cli/prisma/migrations/20221110210035_add_display_name/migration.sql b/packages/cli/prisma/migrations/20221110210035_add_display_name/migration.sql new file mode 100644 index 00000000..adc6ef32 --- /dev/null +++ b/packages/cli/prisma/migrations/20221110210035_add_display_name/migration.sql @@ -0,0 +1,12 @@ +/* + Warnings: + + - A unique constraint covering the columns `[name]` on the table `List` will be added. If there are existing duplicate values, this will fail. + - Added the required column `displayName` to the `List` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "List" ADD COLUMN "displayName" TEXT NOT NULL; + +-- CreateIndex +CREATE UNIQUE INDEX "List_name_key" ON "List"("name"); diff --git a/packages/cli/prisma/schema.prisma b/packages/cli/prisma/schema.prisma index 4386aae6..fd0e00fc 100644 --- a/packages/cli/prisma/schema.prisma +++ b/packages/cli/prisma/schema.prisma @@ -96,20 +96,27 @@ model Click { model List { id String @id @default(cuid()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + isDefault Boolean @default(false) name String + displayName String organizationId String Organization Organization @relation(fields: [organizationId], references: [id]) Member Member[] + @@unique([name]) @@index([organizationId]) } model Member { - id String @id @default(cuid()) - email String - listId String - List List @relation(fields: [listId], references: [id]) - status MemberStatus + id String @id @default(cuid()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + email String + listId String + List List @relation(fields: [listId], references: [id]) + status MemberStatus @@unique([listId, email]) } diff --git a/packages/cli/src/commands/__test__/__snapshots__/exportPreviews.test.ts.snap b/packages/cli/src/commands/__test__/__snapshots__/exportPreviews.test.ts.snap index ad2ed019..7b427d0e 100644 --- a/packages/cli/src/commands/__test__/__snapshots__/exportPreviews.test.ts.snap +++ b/packages/cli/src/commands/__test__/__snapshots__/exportPreviews.test.ts.snap @@ -29,3 +29,76 @@ mailing ✅ Processed 6 previews " `; + +exports[`exportPreviews command outputs html files to outDir 1`] = ` +[MockFunction] { + "calls": [ + [ + "Exporting preview html to", + ], + [ + "./out/", + ], + [ + " |-- account_created_account_created.html", + ], + [ + " |-- new_sign_in_new_sign_in.html", + ], + [ + " |-- reservation_reservation_changed.html", + ], + [ + " |-- reservation_reservation_confirmed.html", + ], + [ + " |-- reservation_reservation_with_error.html", + ], + [ + " |-- reset_password_reset_password.html", + ], + [ + "✅ Processed 6 previews +", + ], + ], + "results": [ + { + "type": "return", + "value": undefined, + }, + { + "type": "return", + "value": undefined, + }, + { + "type": "return", + "value": undefined, + }, + { + "type": "return", + "value": undefined, + }, + { + "type": "return", + "value": undefined, + }, + { + "type": "return", + "value": undefined, + }, + { + "type": "return", + "value": undefined, + }, + { + "type": "return", + "value": undefined, + }, + { + "type": "return", + "value": undefined, + }, + ], +} +`; diff --git a/packages/cli/src/commands/__test__/exportPreviews.test.ts b/packages/cli/src/commands/__test__/exportPreviews.test.ts index 43f4d556..104a883b 100644 --- a/packages/cli/src/commands/__test__/exportPreviews.test.ts +++ b/packages/cli/src/commands/__test__/exportPreviews.test.ts @@ -25,8 +25,7 @@ describe("exportPreviews command", () => { skipLint: true, } as ExportPreviewsArgs); expect(error).not.toHaveBeenCalled(); - expect(log).toHaveBeenCalledWith("Exporting preview html to"); - expect(log).toHaveBeenCalledWith("✅ Processed 6 previews\n"); + expect(log).toMatchSnapshot(); }); it("errors without emails dir", async () => { diff --git a/packages/cli/src/commands/preview/server/setup.ts b/packages/cli/src/commands/preview/server/setup.ts index 11ff91e3..08293d84 100644 --- a/packages/cli/src/commands/preview/server/setup.ts +++ b/packages/cli/src/commands/preview/server/setup.ts @@ -1,4 +1,5 @@ -import { resolve, posix, relative } from "path"; +import { resolve, posix, join } from "path"; +import { tmpdir } from "os"; import { execSync } from "child_process"; import { copy, @@ -10,6 +11,7 @@ import { readFile, appendFile, readJSONSync, + move, } from "fs-extra"; import { debug, log } from "../../../util/log"; @@ -156,22 +158,24 @@ export async function bootstrapMailingDir() { debug("versions do not match, copying .mailing from", nodeMailingPath); await rm(mailingPath, { recursive: true, force: true }); - await mkdir(mailingPath, { recursive: true }); if (process.env.MM_DEV) { - // copy directory contents so that it works in packages/cli - // otherwise there will be an error because .mailing is in the dir being copied - const copies = (await readdir(nodeMailingPath)) - .filter((path) => !DOT_MAILING_IGNORE_REGEXP.test(path)) - .map(async (p) => - copy(p, mailingPath + "/" + relative(".", p), { - recursive: true, - dereference: true, - overwrite: true, - }) - ); - await Promise.all(copies); + // copy directory contents via a temp directory intermediary + // so that it works in packages/cli, otherwise there will be an + // error because .mailing is in the dir being copied + + const tmpDir = join(tmpdir(), `mailing${Date.now()}`); + + await copy(nodeMailingPath, tmpDir, { + recursive: true, + dereference: true, + overwrite: true, + filter: (path) => !DOT_MAILING_IGNORE_REGEXP.test(path), + }); + + await move(tmpDir, mailingPath); } else { + await mkdir(mailingPath, { recursive: true }); await copy(nodeMailingPath, mailingPath, { recursive: true, dereference: true, diff --git a/packages/cli/src/commands/preview/server/start.ts b/packages/cli/src/commands/preview/server/start.ts index 7e4ad5b8..a52aecb4 100644 --- a/packages/cli/src/commands/preview/server/start.ts +++ b/packages/cli/src/commands/preview/server/start.ts @@ -124,7 +124,8 @@ export default async function startPreviewServer() { .listen(port, async () => { clearTimeout(loadLag); log(`running preview at ${currentUrl}`); - if (!quiet && !process.env.MAILING_CI) await open(currentUrl); + if (!quiet && !process.env.MAILING_INTEGRATION_TEST) + await open(currentUrl); }) .on("error", async function onServerError(e: NodeJS.ErrnoException) { if (e.code === "EADDRINUSE") { diff --git a/packages/cli/src/pages/components/FormError.tsx b/packages/cli/src/components/FormError.tsx similarity index 57% rename from packages/cli/src/pages/components/FormError.tsx rename to packages/cli/src/components/FormError.tsx index 241e6470..2ab33631 100644 --- a/packages/cli/src/pages/components/FormError.tsx +++ b/packages/cli/src/components/FormError.tsx @@ -4,7 +4,7 @@ export default function FormError(props: { children?: ReactNode }) { if (!props.children) return null; return ( -
+
{props.children}
); diff --git a/packages/cli/src/components/FormSuccess.tsx b/packages/cli/src/components/FormSuccess.tsx new file mode 100644 index 00000000..47e7c08e --- /dev/null +++ b/packages/cli/src/components/FormSuccess.tsx @@ -0,0 +1,11 @@ +import { ReactNode } from "react"; + +export default function FormSuccess(props: { children?: ReactNode }) { + if (!props.children) return null; + + return ( +
+ {props.children} +
+ ); +} diff --git a/packages/cli/src/components/NavBar/NavBar.tsx b/packages/cli/src/components/NavBar/NavBar.tsx index c4ab5867..48135293 100644 --- a/packages/cli/src/components/NavBar/NavBar.tsx +++ b/packages/cli/src/components/NavBar/NavBar.tsx @@ -10,51 +10,46 @@ type NavBarProps = { children: React.ReactNode }; const NavBar: React.FC = ({ children }) => { const router = useRouter(); - const { nav } = router.query; return (
- {(nav || - (process.env.NODE_ENV !== "development" && - process.env.AUDIENCE_FEATURE_FLAG)) && ( - +
{children}
); }; diff --git a/packages/cli/src/components/NavBar/NavBarButton.tsx b/packages/cli/src/components/NavBar/NavBarButton.tsx index f51973a0..2bb41f60 100644 --- a/packages/cli/src/components/NavBar/NavBarButton.tsx +++ b/packages/cli/src/components/NavBar/NavBarButton.tsx @@ -19,7 +19,7 @@ const NavBar: React.FC = ({ active, href, Icon, name }) => { className={cx( "transition-transform active:scale-90 min-h-[36px] min-w-[36px] flex items-center justify-center rounded-2xl hover:bg-gray-700", { - "bg-gray-500": active, + "bg-white hover:bg-white": active, } )} > diff --git a/packages/cli/src/components/NavBar/__test__/NavBar.test.tsx b/packages/cli/src/components/NavBar/__test__/NavBar.test.tsx index 6c9da31c..16d3c043 100644 --- a/packages/cli/src/components/NavBar/__test__/NavBar.test.tsx +++ b/packages/cli/src/components/NavBar/__test__/NavBar.test.tsx @@ -31,18 +31,4 @@ describe("NavBar", () => { }); expect(previewsLink).toBeVisible(); }); - - describe("development mode", () => { - beforeAll(() => { - (process.env as any).NODE_ENV = "development"; - }); - afterAll(() => { - (process.env as any).NODE_ENV = "test"; - }); - it("does not render navigation in dev", () => { - const { queryByRole } = setup(test); - const nav = queryByRole("navigation"); - expect(nav).toBeNull(); - }); - }); }); diff --git a/packages/cli/src/components/Watermark.tsx b/packages/cli/src/components/Watermark.tsx new file mode 100644 index 00000000..c695047d --- /dev/null +++ b/packages/cli/src/components/Watermark.tsx @@ -0,0 +1,19 @@ +import Image from "next/image"; + +const Watermark: React.FC = () => { + return ( +
+ Mailing icon +

+ Powered by Mailing +

+
+ ); +}; + +export default Watermark; diff --git a/packages/cli/src/components/icons/IconGear.tsx b/packages/cli/src/components/icons/IconGear.tsx index e59a1c06..7b97bb33 100644 --- a/packages/cli/src/components/icons/IconGear.tsx +++ b/packages/cli/src/components/icons/IconGear.tsx @@ -1,6 +1,6 @@ export default function IconEye({ fill }: IconProps) { return ( - + void; + small?: boolean; + white?: boolean; + full?: boolean; +}; + +const Button: React.FC = ({ + text, + href, + onClick, + type, + small, + white, + full, +}) => { + const sharedClasses = cx( + "rounded-2xl border-transparent font-bold leading-none text-black ease-in duration-150", + { + "text-sm pt-2 pb-3 px-3": small, + "text-base pt-3 pb-4 px-4": !small, + "bg-white": white, + "bg-blue": !white, + "w-full": full, + "": !full, + } + ); + return href ? ( + + {text} + + ) : ( + + ); +}; + +export default Button; diff --git a/packages/cli/src/pages/components/ui/Input.tsx b/packages/cli/src/components/ui/Input.tsx similarity index 72% rename from packages/cli/src/pages/components/ui/Input.tsx rename to packages/cli/src/components/ui/Input.tsx index e4bf14e9..796e2c3c 100644 --- a/packages/cli/src/pages/components/ui/Input.tsx +++ b/packages/cli/src/components/ui/Input.tsx @@ -9,7 +9,7 @@ type InputProps = { }; const Input = forwardRef( - ({ label, type, name, id, placeholder }, ref) => ( + ({ label, placeholder, type, name, id }, ref) => ( <>
+ +
+ ); +}; + +export default PaginationControl; diff --git a/packages/cli/src/components/ui/Table.tsx b/packages/cli/src/components/ui/Table.tsx new file mode 100644 index 00000000..8b7138d0 --- /dev/null +++ b/packages/cli/src/components/ui/Table.tsx @@ -0,0 +1,44 @@ +import { ReactElement } from "react"; +import cx from "classnames"; + +type TableProps = { + rows: (string | ReactElement)[][]; +}; + +const Table: React.FC = ({ rows }) => { + // make sure all rows have 3 columns (we can make it more flexible later) + const numColumns = 3; + if (rows.find((row) => row.length !== numColumns)) { + throw new Error("All rows must have the same number of columns"); + } + + return ( +
+ {rows[0].map((header, i) => ( +
+ {header} +
+ ))} + {rows.slice(1).map((row, i) => + row.map((cell, j) => ( +
+ {cell} +
+ )) + )} +
+ ); +}; + +export default Table; diff --git a/packages/cli/src/generator_templates/js/emails/AccountCreated.jsx b/packages/cli/src/generator_templates/js/emails/AccountCreated.jsx index 6e3c231f..f0465678 100644 --- a/packages/cli/src/generator_templates/js/emails/AccountCreated.jsx +++ b/packages/cli/src/generator_templates/js/emails/AccountCreated.jsx @@ -71,7 +71,7 @@ const AccountCreated = ({ name }) => ( -