diff --git a/.env.sample b/.env.sample index 271166b07..14b7e579e 100644 --- a/.env.sample +++ b/.env.sample @@ -15,7 +15,7 @@ GOOGLE_CLIENT_APIKEY= GOOGLE_SERVER_APIKEY= # Google Analytics ID -GOOGLE_TRACKING_ID= +GOOGLE_MEASUREMENT_ID= # JSON Web Token secret to encrypt ID token cookie JWT_SECRET= diff --git a/.eslintrc.js b/.eslintrc.js index df8ff59a4..fecd7b37c 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -99,6 +99,8 @@ module.exports = { "react/static-property-placement": "off", "import/no-relative-packages": "off", "import/no-import-module-exports": "off", + "no-use-before-define": "off", + "@typescript-eslint/no-use-before-define": ["error"], }, settings: { diff --git a/.flowconfig b/.flowconfig deleted file mode 100644 index f111649a9..000000000 --- a/.flowconfig +++ /dev/null @@ -1,11 +0,0 @@ -[ignore] -.*/build -.*/docs -.*/node_modules -.*/public - -[include] - -[options] -module.system.node.resolve_dirname=node_modules -module.system.node.resolve_dirname=src diff --git a/.mocharc.js b/.mocharc.js deleted file mode 100644 index f5f5c38ac..000000000 --- a/.mocharc.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - extension: ["ts"], - require: ["./test/setup"], - exit: true, - file: "./test/mocha-setup", - timeout: 4000, -}; diff --git a/.mocharc.json b/.mocharc.json new file mode 100644 index 000000000..fd357a35d --- /dev/null +++ b/.mocharc.json @@ -0,0 +1,13 @@ +{ + "extension": [ + "ts" + ], + "recursive": true, + "require": [ + "ts-node/register", + "source-map-support/register", + "./test/setup.ts" + ], + "exit": true, + "file": "./test/mocha-setup.ts" +} \ No newline at end of file diff --git a/.nycrc b/.nycrc index 81563e03f..c2cfec7ed 100644 --- a/.nycrc +++ b/.nycrc @@ -1,7 +1,11 @@ { - "require": ["@babel/register"], - "include": ["src"], - "reporter": ["lcov", "text"], - "sourceMap": false, - "instrument": false -} + "extends": "@istanbuljs/nyc-config-typescript", + "all": true, + "include": [ + "src" + ], + "reporter": [ + "lcov", + "text" + ] +} \ No newline at end of file diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index ef48a206b..000000000 --- a/.travis.yml +++ /dev/null @@ -1,18 +0,0 @@ -language: node_js -node_js: - - "stable" - - "8" - - "6" -env: - - CXX=g++-4.8 -addons: - apt: - sources: - - ubuntu-toolchain-r-test - packages: - - g++-4.8 -cache: yarn -script: - - yarn lint - - yarn test - - yarn build --release diff --git a/babel.config.js b/babel.config.js deleted file mode 100644 index d505be62a..000000000 --- a/babel.config.js +++ /dev/null @@ -1,37 +0,0 @@ -/** - * React Starter Kit (https://www.reactstarterkit.com/) - * - * Copyright © 2014-present Kriasoft, LLC. All rights reserved. - * - * This source code is licensed under the MIT license found in the - * LICENSE.txt file in the root directory of this source tree. - */ - -// Babel configuration -// https://babeljs.io/docs/usage/api/ -module.exports = { - plugins: [ - "@babel/plugin-syntax-dynamic-import", - "@babel/plugin-transform-modules-commonjs", - // Decorators - ["@babel/plugin-proposal-decorators", { version: "legacy" }], - ], - presets: [ - ["@babel/preset-typescript", { allowDeclareFields: true }], - [ - "@babel/preset-env", - { - targets: { - node: "current", - }, - }, - ], - "@babel/preset-react", - ], - ignore: ["node_modules", "build"], - env: { - test: { - plugins: ["istanbul"], - }, - }, -}; diff --git a/global.d.ts b/global.d.ts index 116640026..366ff04a2 100644 --- a/global.d.ts +++ b/global.d.ts @@ -4,6 +4,10 @@ declare const __DEV__: boolean; /// /// +declare interface Window { + swUpdate?: boolean; +} + type Dispose = () => void; type InsertCssItem = () => Dispose; type GetCSSItem = () => string; @@ -30,3 +34,8 @@ declare module "*.png" { const value: string; export default value; } + +declare module "*.svg" { + const value: string; + export default value; +} diff --git a/jest/fileTransformer.js b/jest/fileTransformer.js deleted file mode 100644 index 7fe90190e..000000000 --- a/jest/fileTransformer.js +++ /dev/null @@ -1,8 +0,0 @@ -/* eslint-disable @typescript-eslint/no-var-requires */ -const path = require("path"); - -module.exports = { - process(src, filename) { - return `module.exports = ${JSON.stringify(path.basename(filename))};`; - }, -}; diff --git a/package.json b/package.json index 0d89641d6..7dbfb02a6 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,8 @@ "@honeybadger-io/js": "^5.0.0", "@popperjs/core": "^2.11.7", "@reduxjs/toolkit": "^1.9.2", + "@sendgrid/helpers": "^7.7.0", + "@sendgrid/mail": "^7.7.0", "bcrypt": "^5.1.0", "body-parser": "^1.18.3", "bootstrap": "^5.2.3", @@ -23,7 +25,6 @@ "connect-flash": "^0.1.1", "connect-session-sequelize": "^7.1.5", "cookie-parser": "^1.4.6", - "core-js": "^3.30.2", "cors": "^2.8.3", "dayjs": "^1.11.7", "dotenv": "^2.0.0", @@ -37,16 +38,16 @@ "fastclick": "^1.0.6", "fbjs": "^3.0.4", "fetch-mock": "^9.11.0", + "flip-toolkit": "^7.1.0", "google-map-react": "^2.2.0", "history": "^5.3.0", "immutability-helper": "^3.1.1", - "isomorphic-fetch": "^3.0.0", "isomorphic-style-loader": "^5.3.2", "jsonwebtoken": "^9.0.0", + "lodash.get": "^4.4.2", "method-override": "^3.0.0", "mocha-junit-reporter": "^2.2.0", "morgan": "^1.10.0", - "node-fetch": "^2.6.9", "normalizr": "^3.2.2", "passport": "^0.6.0", "passport-google-oauth20": "^2.0.0", @@ -54,7 +55,7 @@ "pg": "^8.9.0", "pretty-error": "^3.0.4", "prop-types": "^15.8.1", - "query-string": "^7.1.3", + "qs": "^6.11.2", "react": "^18.2.0", "react-autosuggest": "^10.0.2", "react-bootstrap": "^2.7.0", @@ -72,31 +73,16 @@ "reserved-usernames": "^1.0.3", "robust-websocket": "^0.2.1", "rotating-file-stream": "^3.1.0", - "sendgrid": "^5.2.3", "sequelize": "^6.29.0", "sequelize-cli": "^6.6.0", "sequelize-typescript": "^2.1.5", "serialize-javascript": "^6.0.1", - "source-map-support": "^0.5.9", "sqlite3": "^5.1.5", "universal-router": "^8.1.0", - "uuid": "^9.0.0", - "whatwg-fetch": "^3.0.0" + "uuid": "^9.0.0" }, "devDependencies": { - "@babel/core": "^7.20.12", - "@babel/eslint-parser": "^7.19.1", - "@babel/node": "^7.20.7", - "@babel/plugin-proposal-class-properties": "^7.18.6", - "@babel/plugin-proposal-decorators": "^7.21.0", - "@babel/plugin-syntax-dynamic-import": "^7.8.3", - "@babel/plugin-transform-modules-commonjs": "^7.20.11", - "@babel/plugin-transform-react-constant-elements": "^7.20.2", - "@babel/plugin-transform-react-inline-elements": "^7.18.6", - "@babel/preset-env": "^7.20.2", - "@babel/preset-react": "^7.18.6", - "@babel/preset-typescript": "^7.21.0", - "@babel/register": "^7.18.9", + "@istanbuljs/nyc-config-typescript": "^1.0.2", "@jedmao/redux-mock-store": "^3.0.5", "@pmmmwh/react-refresh-webpack-plugin": "^0.5.10", "@redux-devtools/extension": "^3.2.5", @@ -116,15 +102,20 @@ "@types/google-map-react": "^2.1.7", "@types/google.analytics": "^0.0.42", "@types/google.maps": "^3.52.1", + "@types/lodash.get": "^4.4.7", "@types/method-override": "^0.0.32", "@types/mocha": "^10.0.1", "@types/morgan": "^1.9.4", - "@types/node-fetch": "^2.6.2", + "@types/node": "^20.2.5", "@types/passport": "^1.0.11", "@types/passport-google-oauth20": "^2.0.11", "@types/passport-local": "^1.0.35", "@types/proxyquire": "^1.3.28", + "@types/react-autosuggest": "^10.1.6", "@types/react-dom": "^18.2.4", + "@types/react-geosuggest": "^2.7.13", + "@types/react-scroll": "^1.8.7", + "@types/serialize-javascript": "^5.0.2", "@types/sinon": "^10.0.15", "@types/supertest": "^2.0.12", "@types/uuid": "^9.0.0", @@ -134,10 +125,6 @@ "@typescript-eslint/parser": "^5.59.1", "assets-webpack-plugin": "^7.1.1", "autoprefixer": "^9.1.5", - "babel-loader": "^9.1.0", - "babel-plugin-istanbul": "^6.1.1", - "babel-plugin-transform-react-remove-prop-types": "^0.4.18", - "babel-plugin-transform-typescript-metadata": "^0.3.2", "browser-sync": "2.29.1", "chai": "4.3.7", "chai-jsdom": "^0.2.3", @@ -188,6 +175,7 @@ "sass-loader": "^13.2.0", "sequelize-mock": "^0.7.0", "sinon": "^15.1.0", + "source-map-support": "^0.5.21", "style-loader": "^0.13.2", "stylelint": "^15.6.2", "stylelint-config-standard-scss": "^9.0.0", @@ -196,7 +184,7 @@ "svg-url-loader": "^8.0.0", "ts-loader": "^9.4.2", "ts-node": "^10.9.1", - "typescript": "^5.0.4", + "typescript": "^5.1.3", "url-loader": "^4.1.1", "webpack": "^5.76.0", "webpack-assets-manifest": "^5.1.0", @@ -220,10 +208,6 @@ "git add --force" ] }, - "nyc": { - "sourceMap": false, - "instrument": false - }, "scripts": { "lint-js": "eslint --ignore-path .gitignore --ignore-pattern \"!**/.*\" .", "lint-css": "stylelint \"src/**/*.{css,less,styl,scss,sass,sss}\"", @@ -231,10 +215,10 @@ "fix-js": "npm run lint-js -- --fix", "fix-css": "npm run lint-css -- --fix", "fix": "npm run fix-js && npm run fix-css", - "test-file": "env TS_NODE_COMPILER_OPTIONS='{\"module\": \"commonjs\" }' mocha", + "test-file": "mocha", "test-file-ci": "npm run test-file --reporter mocha-junit-reporter", - "test": "npm run test-file \"./src/**/*.test.{js,ts}\"", - "test-ci": "npm run test-file-ci \"./src/**/*.test.{js,ts}\"", + "test": "npm run test-file \"./src/**/*.test.ts\"", + "test-ci": "npm run test-file-ci \"./src/**/*.test.ts\"", "test-watch": "npm run test --watch --notify", "test-cover": "nyc npm run test", "coverage": "npm run test-cover && open-cli coverage/lcov-report/index.html", @@ -254,15 +238,15 @@ "integration-file": "cross-env NODE_ENV=test npm-run-all integration-setup -r -p serve \"cypress:run-file {1}\" --", "integration-interactive": "cross-env NODE_ENV=test npm-run-all integration-setup -r -p serve cypress:open", "integration-test-ci": "cross-env NODE_ENV=test npm-run-all integration-setup -r -p serve cypress:run-integration", - "clean": "babel-node tools/run clean", - "copy": "babel-node tools/run copy", - "bundle": "babel-node tools/run bundle", - "build": "babel-node tools/run build", + "clean": "ts-node tools/run clean", + "copy": "ts-node tools/run copy", + "bundle": "ts-node tools/run bundle", + "build": "ts-node tools/run build", "build-stats": "npm run build -- --release --analyse", - "deploy": "babel-node tools/run deploy", - "render": "babel-node tools/run render", - "serve": "babel-node tools/run runServer || true", - "start": "babel-node tools/run start", + "deploy": "ts-node tools/run deploy", + "render": "ts-node tools/run render", + "serve": "ts-node tools/run runServer || true", + "start": "ts-node tools/run start", "prepare": "husky install" }, "packageManager": "yarn@3.5.1" diff --git a/src/DOMUtils.js b/src/DOMUtils.ts similarity index 72% rename from src/DOMUtils.js rename to src/DOMUtils.ts index 7e11825ed..210acf01e 100644 --- a/src/DOMUtils.js +++ b/src/DOMUtils.ts @@ -7,7 +7,13 @@ * LICENSE.txt file in the root directory of this source tree. */ -export function updateTag(tagName, keyName, keyValue, attrName, attrValue) { +export function updateTag( + tagName: string, + keyName: string, + keyValue: string, + attrName: string, + attrValue: string +) { const node = document.head.querySelector( `${tagName}[${keyName}="${keyValue}"]` ); @@ -15,7 +21,7 @@ export function updateTag(tagName, keyName, keyValue, attrName, attrValue) { // Remove and create a new tag in order to make it work with bookmarks in Safari if (node) { - node.parentNode.removeChild(node); + node.parentNode?.removeChild(node); } if (typeof attrValue === "string") { const nextNode = document.createElement(tagName); @@ -25,14 +31,14 @@ export function updateTag(tagName, keyName, keyValue, attrName, attrValue) { } } -export function updateMeta(name, content) { +export function updateMeta(name: string, content: string) { updateTag("meta", "name", name, "content", content); } -export function updateCustomMeta(property, content) { +export function updateCustomMeta(property: string, content: string) { updateTag("meta", "property", property, "content", content); } -export function updateLink(rel, href) { +export function updateLink(rel: string, href: string) { updateTag("link", "rel", rel, "href", href); } diff --git a/src/actions/decisions.ts b/src/actions/decisions.ts index 4ebbc2326..27870df17 100644 --- a/src/actions/decisions.ts +++ b/src/actions/decisions.ts @@ -2,7 +2,7 @@ import { ThunkAction } from "@reduxjs/toolkit"; import { processResponse, credentials, jsonHeaders } from "../core/ApiClient"; import { Action, Decision, State } from "../interfaces"; -export function invalidateDecisions() { +export function invalidateDecisions(): Action { return { type: "INVALIDATE_DECISIONS" }; } diff --git a/src/actions/modals.ts b/src/actions/modals.ts index b86a5a270..8ac4675c7 100644 --- a/src/actions/modals.ts +++ b/src/actions/modals.ts @@ -1,12 +1,13 @@ import { Action, ConfirmOpts, PastDecisionsOpts } from "../interfaces"; +export function showModal(name: string): Action; export function showModal( name: "pastDecisions", - opts: PastDecisionsOpts + opts?: PastDecisionsOpts ): Action; -export function showModal(name: "confirm", opts: ConfirmOpts): Action; +export function showModal(name: "confirm", opts?: ConfirmOpts): Action; -export function showModal(name: unknown, opts: unknown): unknown { +export function showModal(name: unknown, opts?: unknown): unknown { return { type: "SHOW_MODAL", name, diff --git a/src/actions/restaurants.ts b/src/actions/restaurants.ts index b766c9310..d5eab3d42 100644 --- a/src/actions/restaurants.ts +++ b/src/actions/restaurants.ts @@ -196,7 +196,12 @@ export function setNameFilter(val: string): Action { }; } -export function fetchRestaurants(): ThunkAction { +export function fetchRestaurants(): ThunkAction< + Promise, + State, + unknown, + Action +> { return (dispatch) => { dispatch(requestRestaurants()); return fetch("/api/restaurants", { @@ -248,7 +253,7 @@ export function addRestaurant( address: string, lat: number, lng: number -): ThunkAction { +): ThunkAction, State, unknown, Action> { const payload: Partial = { name, placeId, @@ -269,7 +274,7 @@ export function addRestaurant( export function removeRestaurant( id: number -): ThunkAction { +): ThunkAction, State, unknown, Action> { return (dispatch) => { dispatch(deleteRestaurant(id)); return fetch(`/api/restaurants/${id}`, { @@ -282,7 +287,7 @@ export function removeRestaurant( export function changeRestaurantName( id: number, name: string -): ThunkAction { +): ThunkAction, State, unknown, Action> { const payload: Partial = { name }; return (dispatch) => { dispatch(renameRestaurant(id, payload)); @@ -295,7 +300,9 @@ export function changeRestaurantName( }; } -export function addVote(id: number): ThunkAction { +export function addVote( + id: number +): ThunkAction, State, unknown, Action> { return (dispatch) => { dispatch(postVote(id)); return fetch(`/api/restaurants/${id}/votes`, { @@ -308,7 +315,7 @@ export function addVote(id: number): ThunkAction { export function removeVote( restaurantId: number, id: number -): ThunkAction { +): ThunkAction, State, unknown, Action> { return (dispatch) => { dispatch(deleteVote(restaurantId, id)); return fetch(`/api/restaurants/${restaurantId}/votes/${id}`, { @@ -321,7 +328,7 @@ export function removeVote( export function addNewTagToRestaurant( restaurantId: number, name: string -): ThunkAction { +): ThunkAction, State, unknown, Action> { return (dispatch) => { dispatch(postNewTagToRestaurant(restaurantId, name)); return fetch(`/api/restaurants/${restaurantId}/tags`, { @@ -336,7 +343,7 @@ export function addNewTagToRestaurant( export function addTagToRestaurant( restaurantId: number, id: number -): ThunkAction { +): ThunkAction, State, unknown, Action> { return (dispatch) => { dispatch(postTagToRestaurant(restaurantId, id)); return fetch(`/api/restaurants/${restaurantId}/tags`, { @@ -351,7 +358,7 @@ export function addTagToRestaurant( export function removeTagFromRestaurant( restaurantId: number, id: number -): ThunkAction { +): ThunkAction, State, unknown, Action> { return (dispatch) => { dispatch(deleteTagFromRestaurant(restaurantId, id)); return fetch(`/api/restaurants/${restaurantId}/tags/${id}`, { diff --git a/src/actions/tags.ts b/src/actions/tags.ts index fff23fe59..6696f53ad 100644 --- a/src/actions/tags.ts +++ b/src/actions/tags.ts @@ -19,7 +19,12 @@ export function receiveTags(json: Tag[]): Action { }; } -export function fetchTags(): ThunkAction { +export function fetchTags(): ThunkAction< + Promise, + State, + unknown, + Action +> { return (dispatch) => { dispatch(requestTags()); return fetch("/api/tags", { @@ -77,7 +82,7 @@ export function tagDeleted(id: number, userId: number): Action { export function removeTag( id: number -): ThunkAction { +): ThunkAction, State, unknown, Action> { return (dispatch, getState) => { dispatch(deleteTag(id)); return fetch(`/api/tags/${id}`, { diff --git a/src/actions/team.ts b/src/actions/team.ts index 8e4386706..95d72728b 100644 --- a/src/actions/team.ts +++ b/src/actions/team.ts @@ -17,6 +17,9 @@ export function teamDeleted(): Action { export function removeTeam(): ThunkAction { return (dispatch, getState) => { const state = getState(); + if (!state.team) { + return Promise.reject(new Error("No team selected")); + } const teamId = state.team.id; const host = state.host; dispatch(deleteTeam()); @@ -49,6 +52,9 @@ export function updateTeam( ): ThunkAction, State, unknown, Action> { return (dispatch, getState) => { const state = getState(); + if (!state.team) { + return Promise.reject(new Error("No team selected")); + } const teamId = state.team.id; const host = state.host; dispatch(patchTeam(payload)); diff --git a/src/actions/teams.ts b/src/actions/teams.ts index bdebdf311..45b3a2f8d 100644 --- a/src/actions/teams.ts +++ b/src/actions/teams.ts @@ -2,7 +2,7 @@ import { ThunkAction } from "@reduxjs/toolkit"; import { processResponse, credentials, jsonHeaders } from "../core/ApiClient"; import { Action, State, Team } from "../interfaces"; -export function postTeam(obj: Team): Action { +export function postTeam(obj: Partial): Action { return { type: "POST_TEAM", team: obj, @@ -17,8 +17,8 @@ export function teamPosted(obj: Team): Action { } export function createTeam( - payload: Team -): ThunkAction { + payload: Partial +): ThunkAction, State, unknown, Action> { return (dispatch) => { dispatch(postTeam(payload)); return fetch("/api/teams", { diff --git a/src/actions/tests/restaurants.test.js b/src/actions/tests/restaurants.test.ts similarity index 78% rename from src/actions/tests/restaurants.test.js rename to src/actions/tests/restaurants.test.ts index 90c8f63b5..35c7dd097 100644 --- a/src/actions/tests/restaurants.test.js +++ b/src/actions/tests/restaurants.test.ts @@ -2,16 +2,20 @@ /* eslint-disable no-unused-expressions, no-underscore-dangle, import/no-duplicates, arrow-body-style */ import { expect } from "chai"; -import { configureMockStore } from "@jedmao/redux-mock-store"; +import { + MockStoreEnhanced, + configureMockStore, +} from "@jedmao/redux-mock-store"; import fetchMock from "fetch-mock"; import thunk from "redux-thunk"; +import { Action, Dispatch, State } from "../../interfaces"; import * as restaurants from "../restaurants"; const middlewares = [thunk]; const mockStore = configureMockStore(middlewares); describe("actions/restaurants", () => { - let store; + let store: MockStoreEnhanced; beforeEach(() => { store = mockStore({}); @@ -32,7 +36,7 @@ describe("actions/restaurants", () => { it("fetches restaurants", () => { store.dispatch(restaurants.fetchRestaurants()); - expect(fetchMock.lastCall()[0]).to.eq("/api/restaurants"); + expect(fetchMock.lastCall()![0]).to.eq("/api/restaurants"); }); }); @@ -45,7 +49,9 @@ describe("actions/restaurants", () => { return store.dispatch(restaurants.fetchRestaurants()).then(() => { const actions = store.getActions(); expect(actions[1].type).to.eq("RECEIVE_RESTAURANTS"); - expect(actions[1].items).to.eql([{ foo: "bar" }]); + expect("items" in actions[1] && actions[1].items).to.eql([ + { foo: "bar" }, + ]); }); }); }); @@ -65,19 +71,11 @@ describe("actions/restaurants", () => { }); describe("addRestaurant", () => { - let name; - let placeId; - let address; - let lat; - let lng; - - beforeEach(() => { - name = "Lab Zero"; - placeId = "12345"; - address = "123 Main"; - lat = 50; - lng = 100; - }); + const name = "Lab Zero"; + const placeId = "12345"; + const address = "123 Main"; + const lat = 50; + const lng = 100; describe("before fetch", () => { beforeEach(() => { @@ -90,7 +88,7 @@ describe("actions/restaurants", () => { .then(() => { const actions = store.getActions(); expect(actions[0].type).to.eq("POST_RESTAURANT"); - expect(actions[0].restaurant).to.eql({ + expect("restaurant" in actions[0] && actions[0].restaurant).to.eql({ name: "Lab Zero", placeId: "12345", address: "123 Main", @@ -105,8 +103,8 @@ describe("actions/restaurants", () => { restaurants.addRestaurant(name, placeId, address, lat, lng) ); - expect(fetchMock.lastCall()[0]).to.eq("/api/restaurants"); - expect(fetchMock.lastCall()[1].body).to.eq( + expect(fetchMock.lastCall()![0]).to.eq("/api/restaurants"); + expect(fetchMock.lastCall()![1]!.body).to.eq( JSON.stringify({ name, placeId, @@ -135,10 +133,7 @@ describe("actions/restaurants", () => { }); describe("removeRestaurant", () => { - let id; - beforeEach(() => { - id = 1; - }); + const id = 1; describe("before fetch", () => { beforeEach(() => { @@ -149,13 +144,13 @@ describe("actions/restaurants", () => { return store.dispatch(restaurants.removeRestaurant(id)).then(() => { const actions = store.getActions(); expect(actions[0].type).to.eq("DELETE_RESTAURANT"); - expect(actions[0].id).to.eq(1); + expect("id" in actions[0] && actions[0].id).to.eq(1); }); }); it("fetches restaurant", () => { store.dispatch(restaurants.removeRestaurant(id)); - expect(fetchMock.lastCall()[0]).to.eq(`/api/restaurants/${id}`); + expect(fetchMock.lastCall()![0]).to.eq(`/api/restaurants/${id}`); }); }); @@ -174,13 +169,8 @@ describe("actions/restaurants", () => { }); describe("changeRestaurantName", () => { - let id; - let name; - - beforeEach(() => { - id = 1; - name = "Lab Zero"; - }); + const id = 1; + const name = "Lab Zero"; describe("before fetch", () => { beforeEach(() => { @@ -193,16 +183,18 @@ describe("actions/restaurants", () => { .then(() => { const actions = store.getActions(); expect(actions[0].type).to.eq("RENAME_RESTAURANT"); - expect(actions[0].id).to.eq(1); - expect(actions[0].restaurant.name).to.eq("Lab Zero"); + expect("id" in actions[0] && actions[0].id).to.eq(1); + expect( + "restaurant" in actions[0] && actions[0].restaurant.name + ).to.eq("Lab Zero"); }); }); it("fetches restaurant", () => { store.dispatch(restaurants.changeRestaurantName(id, name)); - expect(fetchMock.lastCall()[0]).to.eq(`/api/restaurants/${id}`); - expect(fetchMock.lastCall()[1].body).to.eq(JSON.stringify({ name })); + expect(fetchMock.lastCall()![0]).to.eq(`/api/restaurants/${id}`); + expect(fetchMock.lastCall()![1]!.body).to.eq(JSON.stringify({ name })); }); }); @@ -223,10 +215,7 @@ describe("actions/restaurants", () => { }); describe("addVote", () => { - let id; - beforeEach(() => { - id = 1; - }); + const id = 1; describe("before fetch", () => { beforeEach(() => { @@ -237,14 +226,14 @@ describe("actions/restaurants", () => { return store.dispatch(restaurants.addVote(id)).then(() => { const actions = store.getActions(); expect(actions[0].type).to.eq("POST_VOTE"); - expect(actions[0].id).to.eq(1); + expect("id" in actions[0] && actions[0].id).to.eq(1); }); }); it("fetches vote", () => { store.dispatch(restaurants.addVote(id)); - expect(fetchMock.lastCall()[0]).to.eq(`/api/restaurants/${id}/votes`); + expect(fetchMock.lastCall()![0]).to.eq(`/api/restaurants/${id}/votes`); }); }); @@ -263,12 +252,8 @@ describe("actions/restaurants", () => { }); describe("removeVote", () => { - let restaurantId; - let id; - beforeEach(() => { - restaurantId = 1; - id = 2; - }); + const restaurantId = 1; + const id = 2; describe("before fetch", () => { beforeEach(() => { @@ -281,15 +266,17 @@ describe("actions/restaurants", () => { .then(() => { const actions = store.getActions(); expect(actions[0].type).to.eq("DELETE_VOTE"); - expect(actions[0].restaurantId).to.eq(1); - expect(actions[0].id).to.eq(2); + expect( + "restaurantId" in actions[0] && actions[0].restaurantId + ).to.eq(1); + expect("id" in actions[0] && actions[0].id).to.eq(2); }); }); it("fetches vote", () => { store.dispatch(restaurants.removeVote(restaurantId, id)); - expect(fetchMock.lastCall()[0]).to.eq( + expect(fetchMock.lastCall()![0]).to.eq( `/api/restaurants/${restaurantId}/votes/${id}` ); }); @@ -312,12 +299,8 @@ describe("actions/restaurants", () => { }); describe("addNewTagToRestaurant", () => { - let id; - let name; - beforeEach(() => { - id = 1; - name = "zap"; - }); + const id = 1; + const name = "zap"; describe("before fetch", () => { beforeEach(() => { @@ -330,16 +313,18 @@ describe("actions/restaurants", () => { .then(() => { const actions = store.getActions(); expect(actions[0].type).to.eq("POST_NEW_TAG_TO_RESTAURANT"); - expect(actions[0].restaurantId).to.eq(1); - expect(actions[0].value).to.eq("zap"); + expect( + "restaurantId" in actions[0] && actions[0].restaurantId + ).to.eq(1); + expect("value" in actions[0] && actions[0].value).to.eq("zap"); }); }); it("fetches tag", () => { store.dispatch(restaurants.addNewTagToRestaurant(id, name)); - expect(fetchMock.lastCall()[0]).to.eq(`/api/restaurants/${id}/tags`); - expect(fetchMock.lastCall()[1].body).to.eq(JSON.stringify({ name })); + expect(fetchMock.lastCall()![0]).to.eq(`/api/restaurants/${id}/tags`); + expect(fetchMock.lastCall()![1]!.body).to.eq(JSON.stringify({ name })); }); }); @@ -360,12 +345,8 @@ describe("actions/restaurants", () => { }); describe("addTagToRestaurant", () => { - let restaurantId; - let id; - beforeEach(() => { - restaurantId = 1; - id = 2; - }); + const restaurantId = 1; + const id = 2; describe("before fetch", () => { beforeEach(() => { @@ -378,18 +359,20 @@ describe("actions/restaurants", () => { .then(() => { const actions = store.getActions(); expect(actions[0].type).to.eq("POST_TAG_TO_RESTAURANT"); - expect(actions[0].restaurantId).to.eq(1); - expect(actions[0].id).to.eq(2); + expect( + "restaurantId" in actions[0] && actions[0].restaurantId + ).to.eq(1); + expect("id" in actions[0] && actions[0].id).to.eq(2); }); }); it("fetches tag", () => { store.dispatch(restaurants.addTagToRestaurant(restaurantId, id)); - expect(fetchMock.lastCall()[0]).to.eq( + expect(fetchMock.lastCall()![0]).to.eq( `/api/restaurants/${restaurantId}/tags` ); - expect(fetchMock.lastCall()[1].body).to.eq(JSON.stringify({ id })); + expect(fetchMock.lastCall()![1]!.body).to.eq(JSON.stringify({ id })); }); }); @@ -410,12 +393,8 @@ describe("actions/restaurants", () => { }); describe("removeTagFromRestaurant", () => { - let restaurantId; - let id; - beforeEach(() => { - restaurantId = 1; - id = 2; - }); + const restaurantId = 1; + const id = 2; describe("before fetch", () => { beforeEach(() => { @@ -428,15 +407,17 @@ describe("actions/restaurants", () => { .then(() => { const actions = store.getActions(); expect(actions[0].type).to.eq("DELETE_TAG_FROM_RESTAURANT"); - expect(actions[0].restaurantId).to.eq(1); - expect(actions[0].id).to.eq(2); + expect( + "restaurantId" in actions[0] && actions[0].restaurantId + ).to.eq(1); + expect("id" in actions[0] && actions[0].id).to.eq(2); }); }); it("fetches tag", () => { store.dispatch(restaurants.removeTagFromRestaurant(restaurantId, id)); - expect(fetchMock.lastCall()[0]).to.eq( + expect(fetchMock.lastCall()![0]).to.eq( `/api/restaurants/${restaurantId}/tags/${id}` ); }); diff --git a/src/actions/tests/tags.test.js b/src/actions/tests/tags.test.ts similarity index 83% rename from src/actions/tests/tags.test.js rename to src/actions/tests/tags.test.ts index f0d34adf2..cd521f84b 100644 --- a/src/actions/tests/tags.test.js +++ b/src/actions/tests/tags.test.ts @@ -2,16 +2,20 @@ /* eslint-disable no-unused-expressions, no-underscore-dangle, import/no-duplicates, arrow-body-style */ import { expect } from "chai"; -import { configureMockStore } from "@jedmao/redux-mock-store"; +import { + MockStoreEnhanced, + configureMockStore, +} from "@jedmao/redux-mock-store"; import fetchMock from "fetch-mock"; import thunk from "redux-thunk"; +import { Action, Dispatch, State } from "../../interfaces"; import * as tags from "../tags"; const middlewares = [thunk]; const mockStore = configureMockStore(middlewares); describe("actions/tags", () => { - let store; + let store: MockStoreEnhanced; beforeEach(() => { store = mockStore({ @@ -36,7 +40,7 @@ describe("actions/tags", () => { it("fetches tags", () => { store.dispatch(tags.fetchTags()); - expect(fetchMock.lastCall()[0]).to.eq("/api/tags"); + expect(fetchMock.lastCall()![0]).to.eq("/api/tags"); }); }); @@ -49,7 +53,9 @@ describe("actions/tags", () => { return store.dispatch(tags.fetchTags()).then(() => { const actions = store.getActions(); expect(actions[1].type).to.eq("RECEIVE_TAGS"); - expect(actions[1].items).to.eql([{ foo: "bar" }]); + expect("items" in actions[1] && actions[1].items).to.eql([ + { foo: "bar" }, + ]); }); }); }); @@ -69,10 +75,7 @@ describe("actions/tags", () => { }); describe("removeTag", () => { - let id; - beforeEach(() => { - id = 1; - }); + const id = 1; describe("before fetch", () => { beforeEach(() => { @@ -83,13 +86,13 @@ describe("actions/tags", () => { return store.dispatch(tags.removeTag(id)).then(() => { const actions = store.getActions(); expect(actions[0].type).to.eq("DELETE_TAG"); - expect(actions[0].id).to.eq(1); + expect("id" in actions[0] && actions[0].id).to.eq(1); }); }); it("fetches restaurant", () => { store.dispatch(tags.removeTag(id)); - expect(fetchMock.lastCall()[0]).to.eq(`/api/tags/${id}`); + expect(fetchMock.lastCall()![0]).to.eq(`/api/tags/${id}`); }); }); @@ -107,7 +110,7 @@ describe("actions/tags", () => { return store.dispatch(tags.removeTag(id)).then(() => { const actions = store.getActions(); expect(actions[1].type).to.eq("TAG_DELETED"); - expect(actions[1].id).to.eq(1); + expect("id" in actions[1] && actions[1].id).to.eq(1); }); }); }); diff --git a/src/actions/tests/teams.test.js b/src/actions/tests/teams.test.ts similarity index 69% rename from src/actions/tests/teams.test.js rename to src/actions/tests/teams.test.ts index cc2081c8a..0af0b74f2 100644 --- a/src/actions/tests/teams.test.js +++ b/src/actions/tests/teams.test.ts @@ -2,26 +2,27 @@ /* eslint-disable no-unused-expressions, no-underscore-dangle, import/no-duplicates, arrow-body-style */ import { expect } from "chai"; -import { configureMockStore } from "@jedmao/redux-mock-store"; +import { + MockStoreEnhanced, + configureMockStore, +} from "@jedmao/redux-mock-store"; import fetchMock from "fetch-mock"; import thunk from "redux-thunk"; +import { Action, Dispatch, State } from "../../interfaces"; import * as teams from "../teams"; const middlewares = [thunk]; const mockStore = configureMockStore(middlewares); describe("actions/teams", () => { - let store; + let store: MockStoreEnhanced; beforeEach(() => { store = mockStore({}); }); describe("createTeam", () => { - let payload; - beforeEach(() => { - payload = { foo: "bar" }; - }); + const payload = { name: "bar" }; describe("before fetch", () => { beforeEach(() => { @@ -32,31 +33,33 @@ describe("actions/teams", () => { return store.dispatch(teams.createTeam(payload)).then(() => { const actions = store.getActions(); expect(actions[0].type).to.eq("POST_TEAM"); - expect(actions[0].team).to.eql({ foo: "bar" }); + expect("team" in actions[0] && actions[0].team).to.eql({ + name: "bar", + }); }); }); it("fetches restaurant", () => { store.dispatch(teams.createTeam(payload)); - expect(fetchMock.lastCall()[0]).to.eq("/api/teams"); - expect(fetchMock.lastCall()[1].body).to.eq(JSON.stringify(payload)); + expect(fetchMock.lastCall()![0]).to.eq("/api/teams"); + expect(fetchMock.lastCall()![1]!.body).to.eq(JSON.stringify(payload)); }); }); describe("success", () => { - let team; + const team = { + foo: "bar", + roles: [ + { + id: 1, + teamId: 2, + userId: 3, + }, + ], + }; + beforeEach(() => { - team = { - foo: "bar", - roles: [ - { - id: 1, - teamId: 2, - userId: 3, - }, - ], - }; fetchMock.mock("*", { data: team, }); @@ -66,7 +69,7 @@ describe("actions/teams", () => { return store.dispatch(teams.createTeam(payload)).then(() => { const actions = store.getActions(); expect(actions[1].type).to.eq("TEAM_POSTED"); - expect(actions[1].team).to.eql(team); + expect("team" in actions[1] && actions[1].team).to.eql(team); }); }); }); diff --git a/src/actions/tests/users.test.js b/src/actions/tests/users.test.ts similarity index 73% rename from src/actions/tests/users.test.js rename to src/actions/tests/users.test.ts index edbf40075..17c9bbe93 100644 --- a/src/actions/tests/users.test.js +++ b/src/actions/tests/users.test.ts @@ -2,20 +2,26 @@ /* eslint-disable no-unused-expressions, no-underscore-dangle, import/no-duplicates, arrow-body-style */ import { expect } from "chai"; -import { configureMockStore } from "@jedmao/redux-mock-store"; +import { + MockStoreEnhanced, + configureMockStore, +} from "@jedmao/redux-mock-store"; import fetchMock from "fetch-mock"; import proxyquire from "proxyquire"; import thunk from "redux-thunk"; +import { Action, Dispatch, State, User } from "../../interfaces"; import * as users from "../users"; const middlewares = [thunk]; const mockStore = configureMockStore(middlewares); describe("actions/users", () => { - let store; + let store: MockStoreEnhanced; beforeEach(() => { - store = mockStore({}); + store = mockStore({ + team: { id: 1 }, + }); }); describe("fetchUsers", () => { @@ -34,7 +40,7 @@ describe("actions/users", () => { it("fetches users", () => { store.dispatch(users.fetchUsers()); - expect(fetchMock.lastCall()[0]).to.eq("/api/users"); + expect(fetchMock.lastCall()![0]).to.eq("/api/users"); }); }); @@ -47,7 +53,9 @@ describe("actions/users", () => { return store.dispatch(users.fetchUsers()).then(() => { const actions = store.getActions(); expect(actions[1].type).to.eq("RECEIVE_USERS"); - expect(actions[1].items).to.eql([{ foo: "bar" }]); + expect("items" in actions[1] && actions[1].items).to.eql([ + { foo: "bar" }, + ]); }); }); }); @@ -67,10 +75,10 @@ describe("actions/users", () => { }); describe("addUser", () => { - let payload; + let payload: Partial; beforeEach(() => { - payload = { foo: "bar" }; + payload = { email: "foo@bar.com" }; }); describe("before fetch", () => { @@ -82,15 +90,17 @@ describe("actions/users", () => { return store.dispatch(users.addUser(payload)).then(() => { const actions = store.getActions(); expect(actions[0].type).to.eq("POST_USER"); - expect(actions[0].user).to.eql({ foo: "bar" }); + expect("user" in actions[0] && actions[0].user).to.eql({ + email: "foo@bar.com", + }); }); }); it("fetches restaurant", () => { store.dispatch(users.addUser(payload)); - expect(fetchMock.lastCall()[0]).to.eq("/api/users"); - expect(fetchMock.lastCall()[1].body).to.eq(JSON.stringify(payload)); + expect(fetchMock.lastCall()![0]).to.eq("/api/users"); + expect(fetchMock.lastCall()![1]!.body).to.eq(JSON.stringify(payload)); }); }); @@ -103,7 +113,9 @@ describe("actions/users", () => { return store.dispatch(users.addUser(payload)).then(() => { const actions = store.getActions(); expect(actions[1].type).to.eq("USER_POSTED"); - expect(actions[1].user).to.eql({ foo: "bar" }); + expect("user" in actions[1] && actions[1].user).to.eql({ + foo: "bar", + }); }); }); }); @@ -123,17 +135,17 @@ describe("actions/users", () => { }); describe("removeUser", () => { - let id; - let proxyUsers; + let id = 1; + const proxyUsers = proxyquire("../users", { + "../selectors/user": { + getCurrentUser: () => { + return { id: 231 }; + }, + }, + }); + beforeEach(() => { id = 1; - proxyUsers = proxyquire("../users", { - "../selectors/user": { - getCurrentUser: () => { - return { id: 231 }; - }, - }, - }); }); describe("before fetch", () => { @@ -145,14 +157,14 @@ describe("actions/users", () => { return store.dispatch(proxyUsers.removeUser(id)).then(() => { const actions = store.getActions(); expect(actions[0].type).to.eq("DELETE_USER"); - expect(actions[0].id).to.eq(1); + expect("id" in actions[0] && actions[0].id).to.eq(1); }); }); it("fetches user", () => { store.dispatch(proxyUsers.removeUser(id)); - expect(fetchMock.lastCall()[0]).to.eq(`/api/users/${id}`); + expect(fetchMock.lastCall()![0]).to.eq(`/api/users/${id}`); }); }); @@ -166,22 +178,22 @@ describe("actions/users", () => { return store.dispatch(proxyUsers.removeUser(id)).then(() => { const actions = store.getActions(); expect(actions[0].type).to.eq("DELETE_USER"); - expect(actions[0].isSelf).to.eq(true); - expect(actions[0].id).to.eq(231); - expect(actions[0].team).to.eq(undefined); + expect("isSelf" in actions[0] && actions[0].isSelf).to.eq(true); + expect("id" in actions[0] && actions[0].id).to.eq(231); + expect("team" in actions[0] && actions[0].team).to.eq(undefined); }); }); }); describe("when team is provided", () => { - let team; + const team = { + slug: "labzero", + }; + beforeEach(() => { store = mockStore({ host: "lunch.pink", }); - team = { - slug: "labzero", - }; fetchMock.mock("*", {}); }); @@ -189,14 +201,16 @@ describe("actions/users", () => { return store.dispatch(proxyUsers.removeUser(id, team)).then(() => { const actions = store.getActions(); expect(actions[0].type).to.eq("DELETE_USER"); - expect(actions[0].team).to.eql({ slug: "labzero" }); - expect(actions[0].id).to.eq(1); + expect("team" in actions[0] && actions[0].team).to.eql({ + slug: "labzero", + }); + expect("id" in actions[0] && actions[0].id).to.eq(1); }); }); it("fetches user with full url", () => { store.dispatch(proxyUsers.removeUser(id, team)); - expect(fetchMock.lastCall()[0]).to.eq( + expect(fetchMock.lastCall()![0]).to.eq( `http://${team.slug}.lunch.pink/api/users/${id}` ); }); @@ -211,7 +225,7 @@ describe("actions/users", () => { return store.dispatch(proxyUsers.removeUser(id)).then(() => { const actions = store.getActions(); expect(actions[1].type).to.eq("USER_DELETED"); - expect(actions[1].id).to.eq(1); + expect("id" in actions[1] && actions[1].id).to.eq(1); }); }); }); @@ -231,19 +245,14 @@ describe("actions/users", () => { }); describe("changeUserRole", () => { - let id; - let roleType; - let proxyUsers; - beforeEach(() => { - id = 1; - roleType = "member"; - proxyUsers = proxyquire("../users", { - "../selectors/user": { - getCurrentUser: () => { - return { id: 231 }; - }, + const id = 1; + const roleType = "member"; + const proxyUsers = proxyquire("../users", { + "../selectors/user": { + getCurrentUser: () => { + return { id: 231 }; }, - }); + }, }); describe("before fetch", () => { @@ -257,16 +266,18 @@ describe("actions/users", () => { .then(() => { const actions = store.getActions(); expect(actions[0].type).to.eq("PATCH_USER"); - expect(actions[0].id).to.eq(1); - expect(actions[0].roleType).to.eq("member"); + expect("id" in actions[0] && actions[0].id).to.eq(1); + expect("roleType" in actions[0] && actions[0].roleType).to.eq( + "member" + ); }); }); it("fetches user", () => { store.dispatch(proxyUsers.changeUserRole(id, roleType)); - expect(fetchMock.lastCall()[0]).to.eq(`/api/users/${id}`); - expect(fetchMock.lastCall()[1].body).to.eq( + expect(fetchMock.lastCall()![0]).to.eq(`/api/users/${id}`); + expect(fetchMock.lastCall()![1]!.body).to.eq( JSON.stringify({ id, type: roleType }) ); }); @@ -283,8 +294,10 @@ describe("actions/users", () => { .then(() => { const actions = store.getActions(); expect(actions[1].type).to.eq("USER_PATCHED"); - expect(actions[1].id).to.eq(1); - expect(actions[1].user).to.eql({ foo: "bar" }); + expect("id" in actions[1] && actions[1].id).to.eq(1); + expect("user" in actions[1] && actions[1].user).to.eql({ + foo: "bar", + }); }); }); }); diff --git a/src/actions/tests/websockets.test.js b/src/actions/tests/websockets.test.ts similarity index 86% rename from src/actions/tests/websockets.test.js rename to src/actions/tests/websockets.test.ts index 2dce9bdc0..7382035db 100644 --- a/src/actions/tests/websockets.test.js +++ b/src/actions/tests/websockets.test.ts @@ -2,16 +2,20 @@ import { expect } from "chai"; import { useFakeTimers } from "sinon"; -import { configureMockStore } from "@jedmao/redux-mock-store"; +import { + MockStoreEnhanced, + configureMockStore, +} from "@jedmao/redux-mock-store"; import thunk from "redux-thunk"; import proxyquire from "proxyquire"; +import { Action, Dispatch, State } from "../../interfaces"; import * as websockets from "../websockets"; const middlewares = [thunk]; const mockStore = configureMockStore(middlewares); describe("actions/websockets", () => { - let store; + let store: MockStoreEnhanced; beforeEach(() => { store = mockStore({}); diff --git a/src/actions/user.ts b/src/actions/user.ts index f6986c9b2..955a7a67c 100644 --- a/src/actions/user.ts +++ b/src/actions/user.ts @@ -2,7 +2,7 @@ import { ThunkAction } from "@reduxjs/toolkit"; import { credentials, jsonHeaders, processResponse } from "../core/ApiClient"; import { Action, State, User } from "../interfaces"; -export function patchCurrentUser(payload: User): Action { +export function patchCurrentUser(payload: Partial): Action { return { type: "PATCH_CURRENT_USER", payload, @@ -17,8 +17,8 @@ export function currentUserPatched(user: User): Action { } export function updateCurrentUser( - payload: User -): ThunkAction { + payload: Partial +): ThunkAction, State, unknown, Action> { return (dispatch) => { dispatch(patchCurrentUser(payload)); return fetch("/api/user", { diff --git a/src/actions/users.ts b/src/actions/users.ts index da4f699a9..c4f9fe6fa 100644 --- a/src/actions/users.ts +++ b/src/actions/users.ts @@ -21,7 +21,12 @@ export function receiveUsers(json: User[]): Action { }; } -export function fetchUsers(): ThunkAction { +export function fetchUsers(): ThunkAction< + Promise, + State, + unknown, + Action +> { return (dispatch) => { dispatch(requestUsers()); return fetch("/api/users", { @@ -130,7 +135,7 @@ export function userPosted(json: User): Action { export function addUser( payload: Partial -): ThunkAction { +): ThunkAction, State, unknown, Action> { return (dispatch) => { dispatch(postUser(payload)); return fetch("/api/users", { @@ -182,10 +187,10 @@ export function changeUserRole( return (dispatch, getState) => { const state = getState(); const team = state.team; - let isSelf = false; - if (getCurrentUser(state)!.id === id) { - isSelf = true; + if (!team) { + return Promise.reject(new Error("No team selected")); } + const isSelf = getCurrentUser(state)!.id === id; dispatch(patchUser(id, type, team, isSelf)); return fetch(`/api/users/${id}`, { method: "PATCH", diff --git a/src/api/main/user.ts b/src/api/main/user.ts index 5250be355..9a18e8947 100644 --- a/src/api/main/user.ts +++ b/src/api/main/user.ts @@ -41,15 +41,17 @@ export default () => { if (fieldCount) { try { if (filteredPayload.password) { - const passwordError = getPasswordError(filteredPayload.password); + const passwordError = getPasswordError( + filteredPayload.password as string | undefined + ); if (passwordError) { return res .status(422) .json({ error: true, data: { message: passwordError } }); } const passwordUpdates = await getUserPasswordUpdates( - req.user, - filteredPayload.password + req.user!, + filteredPayload.password as string ); Object.assign(filteredPayload, passwordUpdates); delete filteredPayload.password; diff --git a/src/api/team/restaurants.ts b/src/api/team/restaurants.ts index b760c8042..9cc011918 100644 --- a/src/api/team/restaurants.ts +++ b/src/api/team/restaurants.ts @@ -1,5 +1,4 @@ import { Response, Router } from "express"; -import fetch from "node-fetch"; import { Restaurant, Vote, Tag } from "../../db"; import { Restaurant as RestaurantInterface } from "../../interfaces"; import checkTeamRole from "../helpers/checkTeamRole"; diff --git a/src/api/team/users.ts b/src/api/team/users.ts index b9bf2a107..a325ea8af 100644 --- a/src/api/team/users.ts +++ b/src/api/team/users.ts @@ -58,7 +58,7 @@ export default () => { let allowed = false; if (user.superuser) { allowed = true; - } else if (currentUserRole.type === "owner") { + } else if (currentUserRole?.type === "owner") { if (user.id === roleToChange.userId) { const otherOwners = await hasOtherOwners(team, roleToChange.userId); if (otherOwners) { @@ -71,7 +71,7 @@ export default () => { } } else if (target === undefined && user.id === roleToChange.userId) { allowed = true; - } else { + } else if (currentUserRole) { allowed = canChangeRole(currentUserRole.type, roleToChange.type, target); } return allowed; diff --git a/src/api/tests/decisions.test.js b/src/api/tests/decisions.test.ts similarity index 84% rename from src/api/tests/decisions.test.js rename to src/api/tests/decisions.test.ts index 51db3d1e0..b377b63bd 100644 --- a/src/api/tests/decisions.test.js +++ b/src/api/tests/decisions.test.ts @@ -2,41 +2,42 @@ /* eslint-disable no-unused-expressions */ import { expect } from "chai"; -import { match, spy, stub } from "sinon"; +import { SinonSpy, match, spy, stub } from "sinon"; import bodyParser from "body-parser"; +import { Response } from "superagent"; import request from "supertest"; -import express from "express"; +import express, { Application, RequestHandler } from "express"; import proxyquire from "proxyquire"; import SequelizeMock from "sequelize-mock"; import mockEsmodule from "../../../test/mockEsmodule"; +import { MakeApp, Team } from "../../interfaces"; const proxyquireStrict = proxyquire.noCallThru(); const dbMock = new SequelizeMock(); describe("api/team/decisions", () => { - let app; - let checkTeamRoleSpy; - let DecisionMock; - let loggedInSpy; - let makeApp; - let broadcastSpy; + let app: Application; + let DecisionMock: SequelizeMockObject; + let checkTeamRoleSpy: SinonSpy; + let loggedInSpy: SinonSpy; + let makeApp: MakeApp; + let broadcastSpy: SinonSpy; beforeEach(() => { DecisionMock = dbMock.define("decision", {}); - checkTeamRoleSpy = spy(() => (req, res, next) => { - req.team = { - // eslint-disable-line no-param-reassign - id: 77, - stub: "Lab Zero", - }; - next(); - }); + checkTeamRoleSpy = spy( + (): RequestHandler => (req, res, next) => { + req.team = { + id: 77, + } as Team; + next(); + } + ); loggedInSpy = spy((req, res, next) => { req.user = { - // eslint-disable-line no-param-reassign id: 231, }; next(); @@ -85,7 +86,7 @@ describe("api/team/decisions", () => { }); describe("5 days of decisions", () => { - let findAllSpy; + let findAllSpy: SinonSpy; beforeEach(() => { findAllSpy = spy(DecisionMock, "findAll"); return request(app).get("/?days=5"); @@ -107,7 +108,7 @@ describe("api/team/decisions", () => { }); describe("success", () => { - let response; + let response: Response; beforeEach((done) => { request(app) .get("/") @@ -128,7 +129,7 @@ describe("api/team/decisions", () => { }); describe("failure", () => { - let response; + let response: Response; beforeEach((done) => { stub(DecisionMock, "findAll").throws("Oh No"); @@ -141,7 +142,9 @@ describe("api/team/decisions", () => { }); it("returns error", () => { - expect(response.error.text).to.contain("Oh No"); + expect( + typeof response.error !== "boolean" && response.error.text + ).to.contain("Oh No"); }); }); }); @@ -160,8 +163,8 @@ describe("api/team/decisions", () => { }); describe("queries", () => { - let destroySpy; - let createSpy; + let destroySpy: SinonSpy; + let createSpy: SinonSpy; beforeEach(() => { destroySpy = spy(DecisionMock, "destroy"); createSpy = spy(DecisionMock, "create"); @@ -187,8 +190,8 @@ describe("api/team/decisions", () => { }); describe("1 day ago", () => { - let destroySpy; - let createSpy; + let destroySpy: SinonSpy; + let createSpy: SinonSpy; beforeEach(() => { destroySpy = spy(DecisionMock, "destroy"); createSpy = spy(DecisionMock, "create"); @@ -223,8 +226,8 @@ describe("api/team/decisions", () => { }); describe("success", () => { - let decisionPostedSpy; - let response; + let decisionPostedSpy: SinonSpy; + let response: Response; beforeEach((done) => { decisionPostedSpy = spy(); app = makeApp({ @@ -260,7 +263,7 @@ describe("api/team/decisions", () => { describe("failure", () => { describe("when destroying", () => { - let response; + let response: Response; beforeEach((done) => { stub(DecisionMock, "scope").throws("Oh No"); @@ -273,12 +276,14 @@ describe("api/team/decisions", () => { }); it("returns error", () => { - expect(response.error.text).to.contain("Oh No"); + expect( + typeof response.error !== "boolean" && response.error.text + ).to.contain("Oh No"); }); }); describe("when creating", () => { - let response; + let response: Response; beforeEach((done) => { app = makeApp({ "../../db": mockEsmodule({ @@ -299,7 +304,8 @@ describe("api/team/decisions", () => { }); it("returns error", () => { - expect(response.error.text).to.exist; + expect(typeof response.error !== "boolean" && response.error.text).to + .exist; }); }); }); @@ -319,7 +325,7 @@ describe("api/team/decisions", () => { }); describe("query", () => { - let findAllSpy; + let findAllSpy: SinonSpy; beforeEach(() => { findAllSpy = spy(DecisionMock, "findAll"); return request(app).delete("/fromToday").send(); @@ -335,8 +341,8 @@ describe("api/team/decisions", () => { }); describe("success", () => { - let decisionsDeletedSpy; - let response; + let decisionsDeletedSpy: SinonSpy; + let response: Response; beforeEach((done) => { decisionsDeletedSpy = spy(); app = makeApp({ @@ -366,7 +372,7 @@ describe("api/team/decisions", () => { }); describe("failure", () => { - let response; + let response: Response; beforeEach((done) => { stub(DecisionMock, "scope").throws("Oh No"); @@ -379,7 +385,9 @@ describe("api/team/decisions", () => { }); it("returns error", () => { - expect(response.error.text).to.contain("Oh No"); + expect( + typeof response.error !== "boolean" && response.error.text + ).to.contain("Oh No"); }); }); }); diff --git a/src/api/tests/teams.test.ts b/src/api/tests/teams.test.ts index 8229460a8..b2ac658d3 100644 --- a/src/api/tests/teams.test.ts +++ b/src/api/tests/teams.test.ts @@ -4,25 +4,25 @@ import { expect } from "chai"; import { match, spy, SinonSpy, stub } from "sinon"; import bodyParser from "body-parser"; -import { HTTPError } from "superagent"; import request, { Response } from "supertest"; -import express, { RequestHandler } from "express"; +import express, { Application, RequestHandler } from "express"; import session, { Session } from "express-session"; import proxyquire from "proxyquire"; import SequelizeMock from "sequelize-mock"; import mockEsmodule from "../../../test/mockEsmodule"; +import { MakeApp } from "../../interfaces"; const proxyquireStrict = proxyquire.noCallThru(); const dbMock = new SequelizeMock(); describe("api/main/teams", () => { - let app: Express.Application; + let app: Application; let RoleMock: SequelizeMockObject; let TeamMock: SequelizeMockObject; let UserMock: SequelizeMockObject; let loggedInSpy: SinonSpy; - let makeApp: (deps?: any, middleware?: RequestHandler) => Express.Application; + let makeApp: MakeApp; let sendMailSpy: SinonSpy; beforeEach(() => { @@ -34,7 +34,6 @@ describe("api/main/teams", () => { loggedInSpy = spy((req, res, next) => { req.user = { - // eslint-disable-line no-param-reassign get: () => undefined, id: 231, $get: () => [], @@ -45,7 +44,7 @@ describe("api/main/teams", () => { sendMailSpy = spy(() => Promise.resolve()); - makeApp = (deps, middleware): Express.Application => { + makeApp = (deps, middleware) => { const teamsApi = proxyquireStrict("../main/teams", { "../../db": mockEsmodule({ Team: TeamMock, @@ -385,7 +384,8 @@ describe("api/main/teams", () => { }); it("returns error", () => { - expect((response.error as HTTPError).text).to.exist; + expect(typeof response.error !== "boolean" && response.error.text).to + .exist; }); }); }); @@ -471,7 +471,9 @@ describe("api/main/teams", () => { }); it("returns error", () => { - expect((response.error as HTTPError).text).to.contain("Oh No"); + expect( + typeof response.error !== "boolean" && response.error.text + ).to.contain("Oh No"); }); }); }); @@ -710,7 +712,9 @@ describe("api/main/teams", () => { }); it("returns error", () => { - expect((response.error as HTTPError).text).to.contain("Oh No"); + expect( + typeof response.error !== "boolean" && response.error.text + ).to.contain("Oh No"); }); }); }); diff --git a/src/api/tests/user.test.js b/src/api/tests/user.test.ts similarity index 90% rename from src/api/tests/user.test.js rename to src/api/tests/user.test.ts index 95a14f471..461bc8fa0 100644 --- a/src/api/tests/user.test.js +++ b/src/api/tests/user.test.ts @@ -2,24 +2,26 @@ /* eslint-disable no-unused-expressions */ import { expect } from "chai"; -import { spy, stub } from "sinon"; +import { SinonSpy, spy, stub } from "sinon"; import bodyParser from "body-parser"; +import { Response } from "superagent"; import request from "supertest"; -import express from "express"; +import express, { Application } from "express"; import proxyquire from "proxyquire"; import SequelizeMock from "sequelize-mock"; import mockEsmodule from "../../../test/mockEsmodule"; +import { MakeApp } from "../../interfaces"; const proxyquireStrict = proxyquire.noCallThru(); const dbMock = new SequelizeMock(); describe("api/main/user", () => { - let app; - let UserMock; - let loggedInSpy; - let makeApp; - let updateSpy; + let app: Application; + let UserMock: SequelizeMockObject; + let loggedInSpy: SinonSpy; + let makeApp: MakeApp; + let updateSpy: SinonSpy; beforeEach(() => { UserMock = dbMock.define("user", {}); @@ -29,7 +31,6 @@ describe("api/main/user", () => { loggedInSpy = spy((req, res, next) => { req.user = { - // eslint-disable-line no-param-reassign get: () => undefined, name: "Old Name", id: 231, @@ -76,7 +77,7 @@ describe("api/main/user", () => { }); describe("without valid parameters", () => { - let response; + let response: Response; beforeEach((done) => { request(app) .patch("/") @@ -108,7 +109,7 @@ describe("api/main/user", () => { }); describe("with bad password", () => { - let response; + let response: Response; beforeEach((done) => { app = makeApp({ "../../helpers/getPasswordError": mockEsmodule({ @@ -168,7 +169,7 @@ describe("api/main/user", () => { }); describe("success", () => { - let response; + let response: Response; beforeEach((done) => { request(app) .patch("/") @@ -190,7 +191,7 @@ describe("api/main/user", () => { }); describe("failure", () => { - let response; + let response: Response; beforeEach((done) => { app = makeApp({ "../helpers/loggedIn": mockEsmodule({ @@ -217,7 +218,9 @@ describe("api/main/user", () => { }); it("returns error", () => { - expect(response.error.text).to.contain("Oh No"); + expect( + typeof response.error !== "boolean" && response.error.text + ).to.contain("Oh No"); }); }); }); diff --git a/src/api/tests/users.test.js b/src/api/tests/users.test.ts similarity index 86% rename from src/api/tests/users.test.js rename to src/api/tests/users.test.ts index b829f63c8..f7faca52d 100644 --- a/src/api/tests/users.test.js +++ b/src/api/tests/users.test.ts @@ -2,12 +2,14 @@ /* eslint-disable no-unused-expressions */ import { expect } from "chai"; -import { match, spy, stub } from "sinon"; +import { SinonSpy, SinonStub, match, spy, stub } from "sinon"; import bodyParser from "body-parser"; +import { Response } from "superagent"; import request from "supertest"; -import express from "express"; +import express, { Application, RequestHandler } from "express"; import proxyquire from "proxyquire"; import SequelizeMock from "sequelize-mock"; +import { MakeApp, Role, Team, User } from "../../interfaces"; import mockEsmodule from "../../../test/mockEsmodule"; const proxyquireStrict = proxyquire.noCallThru(); @@ -15,16 +17,16 @@ const proxyquireStrict = proxyquire.noCallThru(); const dbMock = new SequelizeMock(); describe("api/team/users", () => { - let app; - let checkTeamRoleSpy; - let InvitationMock; - let RoleMock; - let UserMock; - let loggedInSpy; - let makeApp; - let broadcastSpy; - let team; - let user; + let app: Application; + let checkTeamRoleSpy: SinonSpy; + let InvitationMock: SequelizeMockObject; + let RoleMock: SequelizeMockObject; + let UserMock: SequelizeMockObject; + let loggedInSpy: SinonSpy; + let makeApp: MakeApp; + let broadcastSpy: SinonSpy; + let team: Team; + let user: User; beforeEach(() => { InvitationMock = dbMock.define("invitation", {}); @@ -33,22 +35,23 @@ describe("api/team/users", () => { team = { id: 77, - get: function get(prop) { + get: function get(prop: K) { return this[prop]; }, - stub: "Lab Zero", - }; - checkTeamRoleSpy = spy(() => (req, res, next) => { - req.team = team; // eslint-disable-line no-param-reassign - next(); - }); + } as Team; + checkTeamRoleSpy = spy( + (): RequestHandler => (req, res, next) => { + req.team = team; // eslint-disable-line no-param-reassign + next(); + } + ); user = { - get: function get(prop) { + get: function get(prop: K) { return this[prop]; }, id: 231, - }; + } as User; loggedInSpy = spy((req, res, next) => { req.user = user; // eslint-disable-line no-param-reassign next(); @@ -107,7 +110,7 @@ describe("api/team/users", () => { }); describe("query", () => { - let scopeSpy; + let scopeSpy: SinonSpy; beforeEach(() => { scopeSpy = spy(UserMock, "scope"); }); @@ -145,7 +148,7 @@ describe("api/team/users", () => { }); describe("success", () => { - let response; + let response: Response; beforeEach((done) => { request(app) .get("/") @@ -166,7 +169,7 @@ describe("api/team/users", () => { }); describe("failure", () => { - let response; + let response: Response; beforeEach((done) => { app = makeApp({ "../../helpers/hasRole": mockEsmodule({ @@ -188,13 +191,15 @@ describe("api/team/users", () => { }); it("returns error", () => { - expect(response.error.text).to.contain("Oh No"); + expect( + typeof response.error !== "boolean" && response.error.text + ).to.contain("Oh No"); }); }); }); describe("POST /", () => { - let sendMailSpy; + let sendMailSpy: SinonSpy; beforeEach(() => { sendMailSpy = spy(() => Promise.resolve()); }); @@ -219,7 +224,7 @@ describe("api/team/users", () => { }); describe("when adding disallowed type", () => { - let response; + let response: Response; beforeEach((done) => { app = makeApp({ "../../helpers/hasRole": mockEsmodule({ @@ -245,7 +250,7 @@ describe("api/team/users", () => { }); describe("when allowed", () => { - let findOneSpy; + let findOneSpy: SinonSpy; beforeEach(() => { app = makeApp({ "../../helpers/hasRole": mockEsmodule({ @@ -267,7 +272,7 @@ describe("api/team/users", () => { }); describe("when user exists and has many roles", () => { - let response; + let response: Response; beforeEach((done) => { stub(UserMock, "findOne").callsFake(() => Promise.resolve({ @@ -313,7 +318,7 @@ describe("api/team/users", () => { }); describe("but is already on team", () => { - let response; + let response: Response; beforeEach((done) => { app = makeApp({ "../../helpers/hasRole": mockEsmodule({ @@ -340,7 +345,7 @@ describe("api/team/users", () => { }); describe("and is not on team", () => { - let createSpy; + let createSpy: SinonSpy; beforeEach(() => { app = makeApp({ "../../helpers/hasRole": mockEsmodule({ @@ -381,9 +386,9 @@ describe("api/team/users", () => { }); describe("when user does not exist", () => { - let createStub; - let destroyStub; - let findOneStub; + let createStub: SinonStub; + let destroyStub: SinonStub; + let findOneStub: SinonStub; beforeEach(() => { app = makeApp({ "../../helpers/hasRole": mockEsmodule({ @@ -454,8 +459,8 @@ describe("api/team/users", () => { }); describe("success", () => { - let userToCreate; - let response; + let userToCreate: Partial; + let response: Response; beforeEach((done) => { userToCreate = { id: 2 }; app = makeApp({ @@ -494,7 +499,7 @@ describe("api/team/users", () => { }); describe("failure", () => { - let response; + let response: Response; beforeEach((done) => { app = makeApp({ "../../helpers/hasRole": mockEsmodule({ @@ -512,13 +517,15 @@ describe("api/team/users", () => { }); it("returns error", () => { - expect(response.error.text).to.contain("Oh No"); + expect( + typeof response.error !== "boolean" && response.error.text + ).to.contain("Oh No"); }); }); }); describe("PATCH /:id", () => { - let hasRole; + let hasRole: Record boolean>; beforeEach(() => { hasRole = mockEsmodule({ default: () => true, @@ -542,8 +549,8 @@ describe("api/team/users", () => { }); describe("when patching self", () => { - let getRoleSpy; - let path; + let getRoleSpy: SinonSpy; + let path: string; beforeEach(() => { getRoleSpy = spy(); app = makeApp({ @@ -562,15 +569,15 @@ describe("api/team/users", () => { }); describe("when owner is changing self", () => { - let role; - let path; + let role: Role; + let path: string; beforeEach(() => { role = { - update: spy(), userId: user.id, teamId: team.id, type: "owner", - }; + } as Role; + role.update = spy(); path = `/${user.id}`; app = makeApp({ "../../helpers/hasRole": hasRole, @@ -581,14 +588,14 @@ describe("api/team/users", () => { }); describe("to member", () => { - let payload; + let payload: Partial; beforeEach(() => { payload = { type: "member" }; }); describe("but there are no other owners", () => { - let findAllStub; - let response; + let findAllStub: SinonStub; + let response: Response; beforeEach((done) => { findAllStub = stub(RoleMock, "findAll").callsFake(() => Promise.resolve([ @@ -638,14 +645,15 @@ describe("api/team/users", () => { }); it("updates role", () => { - expect(role.update.calledWith({ type: "member" })).to.be.true; + expect((role.update as SinonSpy).calledWith({ type: "member" })).to + .be.true; }); }); }); }); describe("when patching other", () => { - let findOneSpy; + let findOneSpy: SinonSpy; beforeEach(() => { findOneSpy = spy(RoleMock, "findOne"); return request(app).patch("/2").send({ type: "member" }); @@ -664,21 +672,21 @@ describe("api/team/users", () => { }); describe("when owner is changing other", () => { - let currentUserRole; - let otherUserId; - let path; - let role; + let currentUserRole: Role; + let otherUserId: number; + let path: string; + let role: Role; beforeEach(() => { currentUserRole = { userId: user.id, teamId: team.id, type: "owner", - }; + } as Role; role = { - update: spy(), userId: otherUserId, teamId: team.id, - }; + } as Role; + role.update = spy(); otherUserId = 2; path = `/${otherUserId}`; app = makeApp({ @@ -697,7 +705,8 @@ describe("api/team/users", () => { }); it("updates role", () => { - expect(role.update.calledWith({ type: "member" })).to.be.true; + expect((role.update as SinonSpy).calledWith({ type: "member" })).to.be + .true; }); }); @@ -709,7 +718,8 @@ describe("api/team/users", () => { }); it("updates role", () => { - expect(role.update.calledWith({ type: "owner" })).to.be.true; + expect((role.update as SinonSpy).calledWith({ type: "owner" })).to.be + .true; }); }); @@ -721,27 +731,28 @@ describe("api/team/users", () => { }); it("updates role", () => { - expect(role.update.calledWith({ type: "owner" })).to.be.true; + expect((role.update as SinonSpy).calledWith({ type: "owner" })).to.be + .true; }); }); }); describe("when member is changing other", () => { - let currentUserRole; - let otherUserId; - let path; - let role; + let currentUserRole: Role; + let otherUserId: number; + let path: string; + let role: Role; beforeEach(() => { currentUserRole = { userId: user.id, teamId: team.id, type: "member", - }; + } as Role; role = { - update: spy(), userId: otherUserId, teamId: team.id, - }; + } as Role; + role.update = spy(); otherUserId = 2; path = `/${otherUserId}`; app = makeApp({ @@ -753,7 +764,7 @@ describe("api/team/users", () => { }); describe("owner", () => { - let response; + let response: Response; beforeEach((done) => { role.type = "owner"; stub(RoleMock, "findOne").callsFake(() => Promise.resolve(role)); @@ -777,7 +788,7 @@ describe("api/team/users", () => { }); describe("member", () => { - let response; + let response: Response; beforeEach((done) => { role.type = "member"; stub(RoleMock, "findOne").callsFake(() => Promise.resolve(role)); @@ -805,12 +816,13 @@ describe("api/team/users", () => { beforeEach(() => request(app).patch(path).send({ type: "member" })); it("updates role", () => { - expect(role.update.calledWith({ type: "member" })).to.be.true; + expect((role.update as SinonSpy).calledWith({ type: "member" })).to + .be.true; }); }); describe("to owner", () => { - let response; + let response: Response; beforeEach((done) => { request(app) .patch(path) @@ -829,7 +841,7 @@ describe("api/team/users", () => { }); describe("when no user is found", () => { - let response; + let response: Response; beforeEach((done) => { stub(RoleMock, "findOne").callsFake(() => Promise.resolve(null)); request(app) @@ -852,18 +864,18 @@ describe("api/team/users", () => { }); describe("success", () => { - let response; - let currentUserRole; - let userToChange; + let response: Response; + let currentUserRole: Role; + let userToChange: User; beforeEach((done) => { currentUserRole = { userId: user.id, teamId: team.id, type: "member", - }; + } as Role; userToChange = { id: 2, - }; + } as User; stub(RoleMock, "findOne").callsFake(() => Promise.resolve({ update: () => Promise.resolve(), @@ -902,7 +914,7 @@ describe("api/team/users", () => { }); describe("failure", () => { - let response; + let response: Response; beforeEach((done) => { app = makeApp({ "../../helpers/hasRole": hasRole, @@ -922,7 +934,9 @@ describe("api/team/users", () => { }); it("returns error", () => { - expect(response.error.text).to.contain("Oh No"); + expect( + typeof response.error !== "boolean" && response.error.text + ).to.contain("Oh No"); }); }); }); @@ -941,25 +955,25 @@ describe("api/team/users", () => { }); describe("success", () => { - let response; - let roleToDestroy; - let currentUserRole; - let userToDelete; + let response: Response; + let roleToDestroy: Role; + let currentUserRole: Role; + let userToDelete: User; beforeEach((done) => { currentUserRole = { userId: user.id, teamId: team.id, type: "member", - }; + } as Role; userToDelete = { id: 2, - }; + } as User; roleToDestroy = { - destroy: spy(() => Promise.resolve()), userId: userToDelete.id, teamId: team.id, type: "guest", - }; + } as Role; + roleToDestroy.destroy = spy(() => Promise.resolve()); stub(RoleMock, "findOne").callsFake(() => Promise.resolve(roleToDestroy) ); @@ -981,7 +995,7 @@ describe("api/team/users", () => { }); it("deletes role", () => { - expect(roleToDestroy.destroy.callCount).to.eq(1); + expect((roleToDestroy.destroy as SinonSpy).callCount).to.eq(1); }); it("returns 204", () => { diff --git a/src/client.tsx b/src/client.tsx index c165d54d8..fced1d19b 100644 --- a/src/client.tsx +++ b/src/client.tsx @@ -7,30 +7,34 @@ * LICENSE.txt file in the root directory of this source tree. */ -import "whatwg-fetch"; import es6Promise from "es6-promise"; import React, { useEffect } from "react"; import { createRoot, hydrateRoot } from "react-dom/client"; -import queryString from "query-string"; +import qs from "qs"; import { Action, createPath, Location } from "history"; -import { ResolveContext } from "universal-router"; import App from "./components/App"; -import createFetch from "./createFetch"; import configureStore from "./store/configureStore"; import history from "./history"; import { updateMeta } from "./DOMUtils"; import routerCreator from "./router"; +import { AppContext, App as AppType } from "./interfaces"; es6Promise.polyfill(); let subdomain: string | undefined; +interface WindowWithApp extends Window { + App: AppType; +} + +declare const window: WindowWithApp; + // Undo Browsersync mangling of host let host = window.App.state.host; if (host.indexOf("//") === 0) { host = host.slice(2); } -const teamSlug = window.App.state.team.slug; +const teamSlug = window.App.state.team?.slug; if (teamSlug && host.indexOf(teamSlug) === 0) { subdomain = teamSlug; host = host.slice(teamSlug.length + 1); // + 1 for dot @@ -52,20 +56,16 @@ const store = configureStore(window.App.state, { history }); // Global (context) variables that can be easily accessed from any React component // https://facebook.github.io/react/docs/context.html -const context: ResolveContext = { +const context: AppContext = { // Enables critical path CSS rendering // https://github.com/kriasoft/isomorphic-style-loader - insertCss: (...styles: Style[]) => { + insertCss: (...styles) => { // eslint-disable-next-line no-underscore-dangle const removeCss = styles.map((x) => x._insertCss()); return () => { removeCss.forEach((f) => f()); }; }, - // Universal HTTP client - fetch: createFetch(fetch, { - baseUrl: window.App.apiUrl, - }), googleApiKey: window.App.googleApiKey, // Initialize a new Redux store // http://redux.js.org/docs/basics/UsageWithReact.html @@ -114,7 +114,7 @@ const onLocationChange = async ({ const isInitialRender = !action; try { context.pathname = location.pathname; - context.query = queryString.parse(location.search); + context.query = qs.parse(location.search, { ignoreQueryPrefix: true }); // Traverses the list of routes in the order they are defined until // it finds the first route that matches provided URL path string @@ -152,9 +152,11 @@ const onLocationChange = async ({ return; } - document.title = route.title; + if (route.title) { + document.title = route.title; + } - updateMeta("description", route.description); + updateMeta("description", route.description || ""); // Update necessary tags in at runtime here, ie: // updateMeta('keywords', route.keywords); // updateCustomMeta('og:url', route.canonicalUrl); @@ -185,8 +187,8 @@ const onLocationChange = async ({ // Google Analytics tracking. Don't send 'pageview' event after // the initial rendering, as it was already sent - if (window.ga) { - window.ga("send", "pageview", createPath(location)); + if (typeof ga === "function") { + ga("send", "pageview", createPath(location)); } }); diff --git a/src/components/App.js b/src/components/App.tsx similarity index 80% rename from src/components/App.js rename to src/components/App.tsx index fd4dfb4e1..3a0886ec8 100644 --- a/src/components/App.js +++ b/src/components/App.tsx @@ -7,11 +7,12 @@ * LICENSE.txt file in the root directory of this source tree. */ -import StyleContext from "isomorphic-style-loader/StyleContext"; +import StyleContext, { InsertCSS } from "isomorphic-style-loader/StyleContext"; import PropTypes from "prop-types"; -import React, { Children } from "react"; +import React, { Children, ReactNode } from "react"; import { Provider as ReduxProvider } from "react-redux"; -import { Loader } from "@googlemaps/js-api-loader"; +import { Libraries, Loader } from "@googlemaps/js-api-loader"; +import { AppContext } from "../interfaces"; import IntlProviderContainer from "./IntlProvider/IntlProviderContainer"; import GoogleMapsLoaderContext from "./GoogleMapsLoaderContext/GoogleMapsLoaderContext"; @@ -19,17 +20,17 @@ const ContextType = { // Enables critical path CSS rendering // https://github.com/kriasoft/isomorphic-style-loader insertCss: PropTypes.func.isRequired, - // Universal HTTP client - fetch: PropTypes.func.isRequired, googleApiKey: PropTypes.string.isRequired, pathname: PropTypes.string.isRequired, query: PropTypes.object, store: PropTypes.object.isRequired, - // Integrate Redux - // http://redux.js.org/docs/basics/UsageWithReact.html - ...ReduxProvider.childContextTypes, }; +interface AppProps { + children: ReactNode; + context: AppContext; +} + /** * The top-level React component setting context (global) variables * that can be accessed from all the child components. @@ -52,22 +53,25 @@ const ContextType = { * container, * ); */ -class App extends React.PureComponent { - static propTypes = { - context: PropTypes.shape(ContextType).isRequired, - children: PropTypes.element.isRequired, +class App extends React.PureComponent { + loaderContextValue: { + loader: Loader; + }; + + styleContextValue: { + insertCss: InsertCSS; }; static childContextTypes = ContextType; - constructor(props) { + constructor(props: AppProps) { super(props); this.loaderContextValue = { loader: new Loader({ apiKey: this.props.context.googleApiKey, version: "weekly", - libraries: ["places", "geocoding"], + libraries: ["places", "geocoding"] as Libraries, }), }; diff --git a/src/components/GoogleInfoWindow/GoogleInfoWindow.scss b/src/components/GoogleInfoWindow/GoogleInfoWindow.scss index b7d252aeb..bc4c00bf0 100644 --- a/src/components/GoogleInfoWindow/GoogleInfoWindow.scss +++ b/src/components/GoogleInfoWindow/GoogleInfoWindow.scss @@ -11,4 +11,5 @@ border: $default-border; padding: 10px; margin-bottom: 10px; + white-space: nowrap; } diff --git a/src/components/GoogleInfoWindow/GoogleInfoWindow.tsx b/src/components/GoogleInfoWindow/GoogleInfoWindow.tsx index 80dc6946d..fe3438c00 100644 --- a/src/components/GoogleInfoWindow/GoogleInfoWindow.tsx +++ b/src/components/GoogleInfoWindow/GoogleInfoWindow.tsx @@ -1,16 +1,9 @@ import React, { Component } from "react"; -import { canUseDOM } from "fbjs/lib/ExecutionEnvironment"; import Button from "react-bootstrap/Button"; import withStyles from "isomorphic-style-loader/withStyles"; import s from "./GoogleInfoWindow.scss"; -let google: typeof window.google; - -if (canUseDOM) { - google = window.google; -} - -interface GoogleInfoWindowProps { +export interface GoogleInfoWindowProps { addRestaurant: (restaurant: google.maps.places.PlaceResult) => void; map: google.maps.Map; placeId: string; @@ -37,7 +30,7 @@ class GoogleInfoWindow extends Component { }; render() { - if (!google) { + if (!window.google) { return null; } @@ -46,7 +39,7 @@ class GoogleInfoWindow extends Component { className={s.root} data-marker style={{ - zIndex: google.maps.Marker.MAX_ZINDEX * 2, + zIndex: google ? google.maps.Marker.MAX_ZINDEX * 2 : 0, }} >
diff --git a/src/components/GoogleInfoWindow/GoogleInfoWindowContainer.ts b/src/components/GoogleInfoWindow/GoogleInfoWindowContainer.ts index effdc520f..35b7effa2 100644 --- a/src/components/GoogleInfoWindow/GoogleInfoWindowContainer.ts +++ b/src/components/GoogleInfoWindow/GoogleInfoWindowContainer.ts @@ -1,11 +1,11 @@ import { connect } from "react-redux"; import { addRestaurant } from "../../actions/restaurants"; import { Dispatch } from "../../interfaces"; -import GoogleInfoWindow from "./GoogleInfoWindow"; +import GoogleInfoWindow, { GoogleInfoWindowProps } from "./GoogleInfoWindow"; const mapDispatchToProps = ( dispatch: Dispatch, - ownProps: { placeId: string } + ownProps: Pick ) => ({ addRestaurant: (result: google.maps.places.PlaceResult) => { // eslint-disable-next-line camelcase diff --git a/src/components/GoogleMapsLoaderContext/GoogleMapsLoaderContext.ts b/src/components/GoogleMapsLoaderContext/GoogleMapsLoaderContext.ts index 54d9085db..f3ffe053a 100644 --- a/src/components/GoogleMapsLoaderContext/GoogleMapsLoaderContext.ts +++ b/src/components/GoogleMapsLoaderContext/GoogleMapsLoaderContext.ts @@ -1,5 +1,10 @@ +import { Loader } from "@googlemaps/js-api-loader"; import { createContext } from "react"; -const GoogleMapsLoaderContext = createContext({}); +export interface IGoogleMapsLoaderContext { + loader?: Loader; +} + +const GoogleMapsLoaderContext = createContext({}); export default GoogleMapsLoaderContext; diff --git a/src/components/Header/Header.tsx b/src/components/Header/Header.tsx index 2b9d47bb3..ddda1f50c 100644 --- a/src/components/Header/Header.tsx +++ b/src/components/Header/Header.tsx @@ -17,7 +17,7 @@ import Link from "../Link/Link"; import lunch from "./lunch.png"; import s from "./Header.scss"; -interface HeaderProps { +export interface HeaderProps { flashes: Flash[]; loggedIn: boolean; path: string; diff --git a/src/components/Header/HeaderContainer.ts b/src/components/Header/HeaderContainer.ts index 8a7505c1c..ea5718cd0 100644 --- a/src/components/Header/HeaderContainer.ts +++ b/src/components/Header/HeaderContainer.ts @@ -1,9 +1,12 @@ import { connect } from "react-redux"; import { State } from "../../interfaces"; import { isLoggedIn } from "../../selectors/user"; -import Header from "./Header"; +import Header, { HeaderProps } from "./Header"; -const mapStateToProps = (state: State, ownProps: { path: string }) => ({ +const mapStateToProps = ( + state: State, + ownProps: Pick +) => ({ flashes: state.flashes, loggedIn: isLoggedIn(state), path: ownProps.path, diff --git a/src/components/HereMarker/HereMarker.js b/src/components/HereMarker/HereMarker.tsx similarity index 100% rename from src/components/HereMarker/HereMarker.js rename to src/components/HereMarker/HereMarker.tsx diff --git a/src/components/HereMarker/package.json b/src/components/HereMarker/package.json index 998c304d6..fb71c985f 100644 --- a/src/components/HereMarker/package.json +++ b/src/components/HereMarker/package.json @@ -2,5 +2,5 @@ "name": "HereMarker", "version": "0.0.0", "private": true, - "main": "./HereMarker.js" -} + "main": "./HereMarker.tsx" +} \ No newline at end of file diff --git a/src/components/Html.js b/src/components/Html.tsx similarity index 74% rename from src/components/Html.js rename to src/components/Html.tsx index 024fecd57..d51625a30 100644 --- a/src/components/Html.js +++ b/src/components/Html.tsx @@ -7,31 +7,28 @@ * LICENSE.txt file in the root directory of this source tree. */ -import PropTypes from "prop-types"; import React, { Component } from "react"; import serialize from "serialize-javascript"; -import config from "../config"; +import * as config from "../config"; +import { App } from "../interfaces"; /* eslint-disable react/no-danger */ -class Html extends Component { - static propTypes = { - app: PropTypes.object, // eslint-disable-line - title: PropTypes.string.isRequired, - ogTitle: PropTypes.string.isRequired, - description: PropTypes.string.isRequired, - root: PropTypes.string, - styles: PropTypes.arrayOf( - PropTypes.shape({ - id: PropTypes.string.isRequired, - cssText: PropTypes.string.isRequired, - }).isRequired - ), - scripts: PropTypes.arrayOf(PropTypes.string.isRequired), - children: PropTypes.string.isRequired, - }; +export interface HtmlProps { + app?: App; + title: string; + ogTitle?: string; + description: string; + root?: string; + styles?: { id: string; cssText: string }[]; + scripts?: string[]; + children: string; +} +class Html extends Component { static defaultProps = { + app: {}, + ogTitle: "", styles: [], scripts: [], root: "", @@ -52,19 +49,24 @@ class Html extends Component { return ( - {config.analytics.googleTrackingId && ( + {config.analytics.googleMeasurementId && ( <>