From ebbb4980cc5daf71d33347b09b23caf0603aae12 Mon Sep 17 00:00:00 2001 From: Akhil G Krishnan Date: Thu, 17 Nov 2022 18:11:29 +0530 Subject: [PATCH 01/55] Bump version to 0.7.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b6d9d1ed3a..a34f61d7b6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@saeloun/miru-web", - "version": "0.6.0", + "version": "0.7.0", "dependencies": { "@babel/plugin-proposal-private-methods": "^7.16.5", "@babel/preset-react": "^7.16.5", From 745d0ef7957d85af96ebf9d187f745417771c5e9 Mon Sep 17 00:00:00 2001 From: Shruti-Apte <72149587+Shruti-Apte@users.noreply.github.com> Date: Fri, 18 Nov 2022 14:58:04 +0530 Subject: [PATCH 02/55] Single invoice download (#780) * Single invoice download functionality added * try-catch block added * pending records migration * download button disabled for draft invoices * more options dropdown added on view invoice and edit invoice page * common function created for download invoice * fixed draft invoice issue --- Gemfile.lock | 1 - app/javascript/src/apis/invoices.ts | 4 +- .../src/components/Invoices/Edit/index.tsx | 14 +++ .../components/Invoices/Invoice/Header.tsx | 10 +- .../Invoices/Invoice/InvoiceActions.tsx | 56 +++++++-- .../src/components/Invoices/Invoice/index.tsx | 2 +- .../Invoices/List/Table/TableRow.tsx | 18 ++- .../common/InvoiceForm/Header/index.tsx | 107 +++++++++++------- .../components/Invoices/common/MoreButton.tsx | 14 +++ .../Invoices/common/MoreOptions.tsx | 24 ++++ .../src/components/Invoices/common/utils.js | 16 +++ 11 files changed, 204 insertions(+), 62 deletions(-) create mode 100644 app/javascript/src/components/Invoices/common/MoreButton.tsx create mode 100644 app/javascript/src/components/Invoices/common/MoreOptions.tsx diff --git a/Gemfile.lock b/Gemfile.lock index b9e7adf8a0..0c07c33a40 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -528,7 +528,6 @@ DEPENDENCIES activerecord-import annotate aws-sdk-s3 - bcrypt (~> 3.1.7) bootsnap (>= 1.4.4) bullet bundler-audit diff --git a/app/javascript/src/apis/invoices.ts b/app/javascript/src/apis/invoices.ts index 6bb17b84df..ca4424b7d5 100644 --- a/app/javascript/src/apis/invoices.ts +++ b/app/javascript/src/apis/invoices.ts @@ -17,11 +17,13 @@ const getInvoice = async (id) => axios.get(`${path}/${id}`); const editInvoice = async (id) => axios.get(`${path}/${id}/edit`); +const downloadInvoice = async (id) => await axios.get(`${path}/${id}/download`, { responseType: "blob" }); + const updateInvoice = async (id, body) => axios.patch(`${path}/${id}/`, body); const sendInvoice = async (id, payload) => axios.post(`${path}/${id}/send_invoice`, payload); -const invoicesApi = { get, post, patch, destroy, sendInvoice, getInvoice, destroyBulk, editInvoice, updateInvoice }; +const invoicesApi = { get, post, patch, destroy, sendInvoice, getInvoice, destroyBulk, editInvoice, updateInvoice, downloadInvoice }; export default invoicesApi; diff --git a/app/javascript/src/components/Invoices/Edit/index.tsx b/app/javascript/src/components/Invoices/Edit/index.tsx index 67de5afdf2..8e60cf35e7 100644 --- a/app/javascript/src/components/Invoices/Edit/index.tsx +++ b/app/javascript/src/components/Invoices/Edit/index.tsx @@ -16,6 +16,7 @@ import SendInvoice from "../common/InvoiceForm/SendInvoice"; import InvoiceTable from "../common/InvoiceTable"; import InvoiceTotal from "../common/InvoiceTotal"; import { generateInvoiceLineItems } from "../common/utils"; +import DeleteInvoice from "../popups/DeleteInvoice"; const EditInvoice = () => { const navigate = useNavigate(); @@ -36,6 +37,8 @@ const EditInvoice = () => { const [issueDate, setIssueDate] = useState(); const [dueDate, setDueDate] = useState(); const [showSendInvoiceModal, setShowSendInvoiceModal] = useState(false); + const [invoiceToDelete, setInvoiceToDelete] = React.useState(null); + const [showDeleteDialog, setShowDeleteDialog] = React.useState(false); const INVOICE_NUMBER_ERROR = "Please enter invoice number to proceed"; const SELECT_CLIENT_ERROR = "Please select client and enter invoice number to proceed"; @@ -127,9 +130,14 @@ const EditInvoice = () => { formType = "edit" handleSaveInvoice={handleSaveInvoice} handleSendInvoice={handleSendInvoice} + deleteInvoice={()=> { + setShowDeleteDialog(true); + setInvoiceToDelete(invoiceDetails.id); + }} setShowInvoiceSetting={false} invoiceNumber={invoiceDetails.invoiceNumber} id={invoiceDetails.id} + />
@@ -191,6 +199,12 @@ const EditInvoice = () => { setIsSending={setShowSendInvoiceModal} handleSaveSendInvoice={handleSaveSendInvoice} />} + {showDeleteDialog && ( + + )} ); } diff --git a/app/javascript/src/components/Invoices/Invoice/Header.tsx b/app/javascript/src/components/Invoices/Invoice/Header.tsx index c14d4f9445..03bc122acc 100644 --- a/app/javascript/src/components/Invoices/Invoice/Header.tsx +++ b/app/javascript/src/components/Invoices/Invoice/Header.tsx @@ -4,7 +4,12 @@ import BackButton from "./BackButton"; import InvoiceActions from "./InvoiceActions"; import InvoiceStatus from "./InvoiceStatus"; -const Header = ({ invoice, handleSendInvoice, setShowDeleteDialog, setInvoiceToDelete }) => ( +const Header = ({ + invoice, + handleSendInvoice, + setShowDeleteDialog, + setInvoiceToDelete +}) => ( <>
@@ -12,12 +17,13 @@ const Header = ({ invoice, handleSendInvoice, setShowDeleteDialog, setInvoiceToD
{ + deleteInvoice={() => { setShowDeleteDialog(true); setInvoiceToDelete(invoice.id); }} editInvoiceLink={`/invoices/${invoice.id}/edit`} sendInvoice={handleSendInvoice} + invoice={invoice} />
diff --git a/app/javascript/src/components/Invoices/Invoice/InvoiceActions.tsx b/app/javascript/src/components/Invoices/Invoice/InvoiceActions.tsx index 42ee5030c0..c356338fb1 100644 --- a/app/javascript/src/components/Invoices/Invoice/InvoiceActions.tsx +++ b/app/javascript/src/components/Invoices/Invoice/InvoiceActions.tsx @@ -1,17 +1,51 @@ -import React from "react"; +import React, { useState, useRef } from "react"; + +import { useOutsideClick } from "helpers"; -import DeleteButton from "./DeleteButton"; import EditButton from "./EditButton"; import SendButton from "./SendButton"; -const InvoiceActions = ({ deleteInvoice, editInvoiceLink, sendInvoice }) => ( - <> -
- - - -
- -); +import MoreButton from "../common/MoreButton"; +import MoreOptions from "../common/MoreOptions"; +import { handleDownloadInvoice } from "../common/utils"; + +const InvoiceActions = ({ + editInvoiceLink, + sendInvoice, + deleteInvoice, + invoice +}) => { + const [isMoreOptionsVisible, setMoreOptionsVisibility] = + useState(false); + + const wrapperRef = useRef(null); + + useOutsideClick( + wrapperRef, + () => setMoreOptionsVisibility(false), + isMoreOptionsVisible + ); + + return ( + <> +
+ + +
+ setMoreOptionsVisibility(!isMoreOptionsVisible)} + /> + {isMoreOptionsVisible && ( + + )} +
+
+ + ); +}; export default InvoiceActions; diff --git a/app/javascript/src/components/Invoices/Invoice/index.tsx b/app/javascript/src/components/Invoices/Invoice/index.tsx index cdce17ffc0..113020bda7 100644 --- a/app/javascript/src/components/Invoices/Invoice/index.tsx +++ b/app/javascript/src/components/Invoices/Invoice/index.tsx @@ -49,7 +49,7 @@ const Invoice = () => { status === InvoiceStatus.SUCCESS && ( <>
+ setInvoiceToDelete={setInvoiceToDelete}/>
diff --git a/app/javascript/src/components/Invoices/List/Table/TableRow.tsx b/app/javascript/src/components/Invoices/List/Table/TableRow.tsx index 3776388318..a23eef60cd 100644 --- a/app/javascript/src/components/Invoices/List/Table/TableRow.tsx +++ b/app/javascript/src/components/Invoices/List/Table/TableRow.tsx @@ -14,6 +14,7 @@ import { Avatar, Badge, Tooltip } from "StyledComponents"; import CustomCheckbox from "common/CustomCheckbox"; import getStatusCssClass from "utils/getBadgeStatus"; +import { handleDownloadInvoice } from "../../common/utils"; import MoreOptions from "../MoreOptions"; import SendInvoice from "../SendInvoice"; @@ -28,7 +29,7 @@ const TableRow = ({ }) => { const [isSending, setIsSending] = useState(false); const [isMenuOpen, setIsMenuOpen] = useState(false); - useDebounce(isMenuOpen,500); + useDebounce(isMenuOpen, 500); const navigate = useNavigate(); const handleCheckboxChange = () => { @@ -58,12 +59,13 @@ const TableRow = ({ /> - navigate(`/invoices/${invoice.id}`)} className="md:w-1/5 md:pr-2 pr-6 py-5 font-medium tracking-wider flex items-center text-left whitespace-nowrap cursor-pointer"> + navigate(`/invoices/${invoice.id}`)} + className="md:w-1/5 md:pr-2 pr-6 py-5 font-medium tracking-wider flex items-center text-left whitespace-nowrap cursor-pointer" + >
- + {invoice.client.name}

@@ -99,7 +101,11 @@ const TableRow = ({ - diff --git a/app/javascript/src/components/Invoices/common/InvoiceForm/Header/index.tsx b/app/javascript/src/components/Invoices/common/InvoiceForm/Header/index.tsx index f797aa8104..77add4e915 100644 --- a/app/javascript/src/components/Invoices/common/InvoiceForm/Header/index.tsx +++ b/app/javascript/src/components/Invoices/common/InvoiceForm/Header/index.tsx @@ -1,26 +1,42 @@ -import React, { Fragment } from "react"; +import React, { Fragment, useState, useRef } from "react"; import { TOASTER_DURATION } from "constants/index"; +import { useOutsideClick } from "helpers"; import { XIcon, FloppyDiskIcon, PaperPlaneTiltIcon, SettingIcon } from "miruIcons"; import { Link } from "react-router-dom"; import { ToastContainer } from "react-toastify"; +import MoreButton from "../../MoreButton"; +import MoreOptions from "../../MoreOptions"; + const Header = ({ formType = "generate", handleSaveInvoice, handleSendInvoice, setShowInvoiceSetting, invoiceNumber = null, - id = null -}) => ( - - -
-
-

{formType == "edit" ? `Edit Invoice #${invoiceNumber}`: "Generate Invoice"}

+ id = null, + deleteInvoice = null +}) => { + + const [isMoreOptionsVisible, setMoreOptionsVisibility] = useState(false); + const wrapperRef = useRef(null); + + useOutsideClick( + wrapperRef, + () => setMoreOptionsVisibility(false), + isMoreOptionsVisible + ); - {formType == "generate" && + return ( + + +
+
+

{formType == "edit" ? `Edit Invoice #${invoiceNumber}`: "Generate Invoice"}

+ + {formType == "generate" && - } -
-
- - - CANCEL - - - + } +
+
+ + + CANCEL + + + +
+ setMoreOptionsVisibility(!isMoreOptionsVisible)} + /> + {isMoreOptionsVisible && ( + + )} +
+
-
- -); + + );}; export default Header; diff --git a/app/javascript/src/components/Invoices/common/MoreButton.tsx b/app/javascript/src/components/Invoices/common/MoreButton.tsx new file mode 100644 index 0000000000..3f46d22997 --- /dev/null +++ b/app/javascript/src/components/Invoices/common/MoreButton.tsx @@ -0,0 +1,14 @@ +import React from "react"; + +import { DotsThreeVertical } from "phosphor-react"; + +const MoreButton = ({ onClick }) => ( + +); + +export default MoreButton; diff --git a/app/javascript/src/components/Invoices/common/MoreOptions.tsx b/app/javascript/src/components/Invoices/common/MoreOptions.tsx new file mode 100644 index 0000000000..b4bb997379 --- /dev/null +++ b/app/javascript/src/components/Invoices/common/MoreOptions.tsx @@ -0,0 +1,24 @@ +import React from "react"; + +import { Trash, DownloadSimple } from "phosphor-react"; + +const MoreOptions = ({ deleteInvoice, downloadInvoice, invoice=null }) => ( +
    + {downloadInvoice != null && invoice.status != "draft" &&
  • + + Download +
  • } +
  • + + Delete +
  • +
+); + +export default MoreOptions; diff --git a/app/javascript/src/components/Invoices/common/utils.js b/app/javascript/src/components/Invoices/common/utils.js index 579d882408..76d11e913d 100644 --- a/app/javascript/src/components/Invoices/common/utils.js +++ b/app/javascript/src/components/Invoices/common/utils.js @@ -2,6 +2,8 @@ import dayjs from "dayjs"; import { lineTotalCalc } from "helpers"; import generateInvoice from "apis/generateInvoice"; +import invoicesApi from "apis/invoices"; +import Toastr from "common/Toastr"; export const generateInvoiceLineItems = (selectedLineItems, manualEntryArr) => { let invoiceLineItems = []; @@ -99,3 +101,17 @@ export const fetchMultipleNewLineItems = async ( setTeamMembers(res.data.filter_options.team_members); setLoading(false); }; + +export const handleDownloadInvoice = async (invoice) => { + try { + const res = await invoicesApi.downloadInvoice(invoice.id); + const url = window.URL.createObjectURL(new Blob([res.data])); + const link = document.createElement("a"); + link.href = url; + link.setAttribute("download", `${invoice.invoiceNumber}.pdf`); + document.body.appendChild(link); + link.click(); + } catch { + Toastr.error("Something went wrong"); + } +}; From b918b10732604ab9e4b13f9813bbaed067a9b644 Mon Sep 17 00:00:00 2001 From: Prasanth Chaduvula Date: Mon, 21 Nov 2022 17:43:29 +0530 Subject: [PATCH 03/55] Added instructions to run elasticsearch on latest macos (#791) * Added instructions to run elasticsearch on lastest macos * Added instructions to run elasticsearch on lastest macos [ci skip] --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index 7da0109cad..48367e8076 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,14 @@ brew install postgresql brew install elastic/tap/elasticsearch-full brew services start elasticsearch-full ``` + To run elasticsearch on latest macos(ventura) please follow the below instructions + - Install Docker Desktop ( M1 / Intel ) https://www.docker.com/products/docker-desktop/ + - Run below command in your terminal & you can check by opening `localhost:9200` + ``` + docker run -p 127.0.0.1:9200:9200 -p 127.0.0.1:9300:9300 -e "discovery.type=single-node" docker.elastic.co/elasticsearch/elasticsearch:7.17.7 + ``` + - Install Chrome Extension to browse the Cluster ( Kind of like PGAdmin for Elastic Search ) https://chrome.google.com/webstore/search/multi%20elastic%20search%20head + More information available at https://www.elastic.co/guide/en/elasticsearch/reference/current/docker.html 7. Install Redis From c0498c8e898ffbcd4166eba8fee5b1d6dc9a407b Mon Sep 17 00:00:00 2001 From: Abinash Date: Mon, 21 Nov 2022 17:50:34 +0530 Subject: [PATCH 04/55] auto expandable input has been added (#779) * auto expandable input has been added * removed autosize package * eslint errors fixed AddEntry.tsx --- .../src/components/TimeTracking/AddEntry.tsx | 13 ++++----- package.json | 2 +- yarn.lock | 29 ++++++++++++++++--- 3 files changed, 31 insertions(+), 13 deletions(-) diff --git a/app/javascript/src/components/TimeTracking/AddEntry.tsx b/app/javascript/src/components/TimeTracking/AddEntry.tsx index 4860ea1ead..480e197313 100644 --- a/app/javascript/src/components/TimeTracking/AddEntry.tsx +++ b/app/javascript/src/components/TimeTracking/AddEntry.tsx @@ -1,11 +1,11 @@ /* eslint-disable @typescript-eslint/no-var-requires */ import React, { useState, useEffect, useRef, MutableRefObject } from "react"; -import autosize from "autosize"; import { format } from "date-fns"; import dayjs from "dayjs"; import { minFromHHMM, minToHHMM, validateTimesheetEntry } from "helpers"; import { useOutsideClick } from "helpers"; +import TextareaAutosize from "react-autosize-textarea"; import { TimeInput } from "StyledComponents"; import timesheetEntryApi from "apis/timesheet-entry"; @@ -139,10 +139,7 @@ const AddEntry: React.FC = ({ }; useEffect(() => { - const textArea = document.querySelector("textarea"); - autosize(textArea); handleFillData(); - textArea.click(); }, []); return ( @@ -196,15 +193,15 @@ const AddEntry: React.FC = ({ ))}
- + className={("w-129 px-1 rounded-sm bg-miru-gray-100 focus:miru-han-purple-1000 outline-none resize-none mt-2 overflow-y-auto " + (editEntryId ? "h-auto" : "h-8") )} + />

diff --git a/package.json b/package.json index a34f61d7b6..9d0651876c 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,6 @@ "alpine-turbo-drive-adapter": "^2.0.0", "alpinejs": "^2.2.5", "autoprefixer": "~9", - "autosize": "^5.0.1", "axios": "^0.24.0", "babel-plugin-js-logger": "^1.0.17", "babel-plugin-transform-react-remove-prop-types": "^0.4.24", @@ -39,6 +38,7 @@ "ramda": "^0.28.0", "react": "^17.0.2", "react-autocomplete": "^1.8.1", + "react-autosize-textarea": "^7.1.0", "react-datepicker": "^4.7.0", "react-dom": "^17.0.2", "react-ga4": "^1.4.1", diff --git a/yarn.lock b/yarn.lock index ea7096ad2e..4e6af14dec 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1975,10 +1975,10 @@ autoprefixer@^9, autoprefixer@^9.6.1, autoprefixer@~9: postcss "^7.0.32" postcss-value-parser "^4.1.0" -autosize@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/autosize/-/autosize-5.0.1.tgz#ed269b0fa9b7eb47627048a1bb3299e99e003a0f" - integrity sha512-UIWUlE4TOVPNNj2jjrU39wI4hEYbneUypEqcyRmRFIx5CC2gNdg3rQr+Zh7/3h6egbBvm33TDQjNQKtj9Tk1HA== +autosize@^4.0.2: + version "4.0.4" + resolved "https://registry.yarnpkg.com/autosize/-/autosize-4.0.4.tgz#924f13853a466b633b9309330833936d8bccce03" + integrity sha512-5yxLQ22O0fCRGoxGfeLSNt3J8LB1v+umtpMnPW6XjkTWXKoN0AmXAIhelJcDtFT/Y/wYWmfE+oqU10Q0b8FhaQ== aws-sign2@~0.7.0: version "0.7.0" @@ -2766,6 +2766,11 @@ compression@^1.7.4: safe-buffer "5.1.2" vary "~1.1.2" +computed-style@~0.1.3: + version "0.1.4" + resolved "https://registry.yarnpkg.com/computed-style/-/computed-style-0.1.4.tgz#7f344fd8584b2e425bedca4a1afc0e300bb05d74" + integrity sha512-WpAmaKbMNmS3OProfHIdJiNleNJdgUrJfbKArXua28QF7+0CoZjlLn0lp6vlc+dl5r2/X9GQiQRQQU4BzSa69w== + concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" @@ -5616,6 +5621,13 @@ lilconfig@^2.0.5: resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.0.5.tgz#19e57fd06ccc3848fd1891655b5a447092225b25" integrity sha512-xaYmXZtTHPAw5m+xLN8ab9C+3a8YmV3asNSPOATITbtwrfbwaLJj8h66H1WMIpALCkqsIzK3h7oQ+PdX+LQ9Eg== +line-height@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/line-height/-/line-height-0.3.1.tgz#4b1205edde182872a5efa3c8f620b3187a9c54c9" + integrity sha512-YExecgqPwnp5gplD2+Y8e8A5+jKpr25+DzMbFdI1/1UAr0FJrTFv4VkHLf8/6B590i1wUPJWMKKldkd/bdQ//w== + dependencies: + computed-style "~0.1.3" + lines-and-columns@^1.1.6: version "1.2.4" resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" @@ -7733,6 +7745,15 @@ react-autocomplete@^1.8.1: dom-scroll-into-view "1.0.1" prop-types "^15.5.10" +react-autosize-textarea@^7.1.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/react-autosize-textarea/-/react-autosize-textarea-7.1.0.tgz#902c84fc395a689ca3a484dfb6bc2be9ba3694d1" + integrity sha512-BHpjCDkuOlllZn3nLazY2F8oYO1tS2jHnWhcjTWQdcKiiMU6gHLNt/fzmqMSyerR0eTdKtfSIqtSeTtghNwS+g== + dependencies: + autosize "^4.0.2" + line-height "^0.3.1" + prop-types "^15.5.6" + react-datepicker@^4.7.0: version "4.7.0" resolved "https://registry.yarnpkg.com/react-datepicker/-/react-datepicker-4.7.0.tgz#75e03b0a6718b97b84287933307faf2ed5f03cf4" From 24e0bc2edc60517cb8bc325486d37fe79b94cf54 Mon Sep 17 00:00:00 2001 From: Akhil G Krishnan Date: Mon, 21 Nov 2022 19:39:33 +0530 Subject: [PATCH 05/55] Has secure token auto generation issue --- app/models/user.rb | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/models/user.rb b/app/models/user.rb index a1120cdade..2dcab58e05 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -86,6 +86,7 @@ class User < ApplicationRecord # Callbacks after_discard :discard_project_members + before_create :set_token # scopes scope :valid_invitations, -> { invitations.where(sender: self).valid_invitations } @@ -136,6 +137,10 @@ def employed_at?(company_id) private + def set_token + self.token = SecureRandom.base58(50) + end + def discard_project_members project_members.discard_all end From 02acf20ee1ffd7c4b2108821313e3acc2fd85075 Mon Sep 17 00:00:00 2001 From: Vipul A M Date: Mon, 21 Nov 2022 21:01:00 +0530 Subject: [PATCH 06/55] Some Cleanups (#788) * Fix Gemfile * Remove empty app/helpers/workspaces_helper.rb * Remove empty app/helpers/workspaces_helper.rb * Get rid if utility functions --- Gemfile.lock | 1 - .../internal_api/v1/invoices_controller.rb | 4 +--- .../internal_api/v1/workspaces_helper.rb | 4 ---- app/helpers/workspaces_helper.rb | 4 ---- app/models/client.rb | 7 ++++--- app/models/project.rb | 5 ++--- .../services/date_range_service.rb | 20 ++++++++----------- app/services/time_entries/filters.rb | 8 +++++--- .../internal_api/v1/workspaces_helper_spec.rb | 17 ---------------- spec/models/client_spec.rb | 3 +-- 10 files changed, 21 insertions(+), 52 deletions(-) delete mode 100644 app/helpers/internal_api/v1/workspaces_helper.rb delete mode 100644 app/helpers/workspaces_helper.rb rename lib/utility_functions.rb => app/services/date_range_service.rb (71%) delete mode 100644 spec/helpers/internal_api/v1/workspaces_helper_spec.rb diff --git a/Gemfile.lock b/Gemfile.lock index 0c07c33a40..d1236bb2be 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -545,7 +545,6 @@ DEPENDENCIES foreman grover hash_dot - honeybadger image_processing (>= 1.2) jbuilder (~> 2.11) letter_opener diff --git a/app/controllers/internal_api/v1/invoices_controller.rb b/app/controllers/internal_api/v1/invoices_controller.rb index 394ff66b17..12e975e7b0 100644 --- a/app/controllers/internal_api/v1/invoices_controller.rb +++ b/app/controllers/internal_api/v1/invoices_controller.rb @@ -4,8 +4,6 @@ class InternalApi::V1::InvoicesController < InternalApi::V1::ApplicationControll before_action :load_client, only: [:create, :update] after_action :ensure_time_entries_billed, only: [:send_invoice] - include UtilityFunctions - def index authorize Invoice pagy, invoices = pagy(invoices_query, items_param: :invoices_per_page) @@ -112,7 +110,7 @@ def invoices_query def from_to_date(from_to) if from_to - range_from_timeframe(from_to[:date_range], from_to[:from], from_to[:to]) + DateRangeService.new(timeframe: from_to[:date_range], from: from_to[:from], to: from_to[:to]).process end end diff --git a/app/helpers/internal_api/v1/workspaces_helper.rb b/app/helpers/internal_api/v1/workspaces_helper.rb deleted file mode 100644 index 7728bd9ef3..0000000000 --- a/app/helpers/internal_api/v1/workspaces_helper.rb +++ /dev/null @@ -1,4 +0,0 @@ -# frozen_string_literal: true - -module InternalApi::V1::WorkspacesHelper -end diff --git a/app/helpers/workspaces_helper.rb b/app/helpers/workspaces_helper.rb deleted file mode 100644 index e6df7c2c0d..0000000000 --- a/app/helpers/workspaces_helper.rb +++ /dev/null @@ -1,4 +0,0 @@ -# frozen_string_literal: true - -module WorkspacesHelper -end diff --git a/app/models/client.rb b/app/models/client.rb index 0ce2194294..ab16b3b714 100644 --- a/app/models/client.rb +++ b/app/models/client.rb @@ -29,7 +29,6 @@ class Client < ApplicationRecord include Discard::Model - include UtilityFunctions has_many :projects has_many :timesheet_entries, through: :projects @@ -46,7 +45,8 @@ def reindex_projects end def total_hours_logged(time_frame = "week") - timesheet_entries.where(work_date: range_from_timeframe(time_frame)).sum(:duration) + timesheet_entries.where(work_date: DateRangeService.new(timeframe: time_frame).process) + .sum(:duration) end def project_details(time_frame = "week") @@ -56,7 +56,8 @@ def project_details(time_frame = "week") name: project.name, billable: project.billable, team: project.project_member_full_names, - minutes_spent: project.timesheet_entries.where(work_date: range_from_timeframe(time_frame)).sum(:duration) + minutes_spent: project.timesheet_entries.where(work_date: DateRangeService.new(timeframe: time_frame).process) + .sum(:duration) } end end diff --git a/app/models/project.rb b/app/models/project.rb index 0bf175e9d2..7b1f7703f8 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -25,7 +25,6 @@ class Project < ApplicationRecord include Discard::Model - include UtilityFunctions # Associations belongs_to :client @@ -55,7 +54,7 @@ def search_data def project_team_member_details(time_frame) entries = timesheet_entries.includes(:user) - .where(user_id: project_members.pluck(:user_id), work_date: range_from_timeframe(time_frame)) + .where(user_id: project_members.pluck(:user_id), work_date: DateRangeService.new(timeframe: time_frame).process) .select(:user_id, "SUM(duration) as duration") .group(:user_id) @@ -80,7 +79,7 @@ def project_member_full_names end def total_hours_logged(time_frame = "week") - timesheet_entries.where(work_date: range_from_timeframe(time_frame)).sum(:duration) + timesheet_entries.where(work_date: DateRangeService.new(timeframe: time_frame).process).sum(:duration) end def overdue_and_outstanding_amounts diff --git a/lib/utility_functions.rb b/app/services/date_range_service.rb similarity index 71% rename from lib/utility_functions.rb rename to app/services/date_range_service.rb index 86d212b760..113514cf8d 100644 --- a/lib/utility_functions.rb +++ b/app/services/date_range_service.rb @@ -1,13 +1,15 @@ # frozen_string_literal: true -module UtilityFunctions - class InvalidDatePassed < StandardError - def message - "Date value passed is invalid" - end +class DateRangeService + attr_reader :timeframe, :from, to + + def initialize(timeframe:, from: nil, to: nil) + @timeframe = timeframe + @from = from + @to = to end - def range_from_timeframe(timeframe, from = nil, to = nil) + def process case timeframe when "last_week" 1.weeks.ago.beginning_of_week..1.weeks.ago.end_of_week @@ -27,10 +29,4 @@ def range_from_timeframe(timeframe, from = nil, to = nil) 0.year.ago.beginning_of_year..0.year.ago.end_of_year end end - - def convert_to_date(value) - raise InvalidDatePassed if value.nil? - - Date.parse(value) - end end diff --git a/app/services/time_entries/filters.rb b/app/services/time_entries/filters.rb index 4cee1e1991..6986a81ced 100644 --- a/app/services/time_entries/filters.rb +++ b/app/services/time_entries/filters.rb @@ -2,8 +2,6 @@ module TimeEntries class Filters < ApplicationService - include UtilityFunctions - FILTER_PARAM_LIST = %i[date_range status team_member client] attr_reader :filter_params @@ -23,7 +21,11 @@ def process end def date_range_filter - { work_date: range_from_timeframe(filter_params[:date_range], filter_params[:from], filter_params[:to]) } + { + work_date: DateRangeService.new( + timeframe: filter_params[:date_range], from: filter_params[:from], + to: filter_params[:to]).process + } end def status_filter diff --git a/spec/helpers/internal_api/v1/workspaces_helper_spec.rb b/spec/helpers/internal_api/v1/workspaces_helper_spec.rb deleted file mode 100644 index 7db4ed80d2..0000000000 --- a/spec/helpers/internal_api/v1/workspaces_helper_spec.rb +++ /dev/null @@ -1,17 +0,0 @@ -# frozen_string_literal: true - -require "rails_helper" - -# Specs in this file have access to a helper object that includes -# the InternalApi::V1::WorkspacesHelper. For example: -# -# describe InternalApi::V1::WorkspacesHelper do -# describe "string concat" do -# it "concats two strings with spaces" do -# expect(helper.concat_strings("this","that")).to eq("this that") -# end -# end -# end -RSpec.describe InternalApi::V1::WorkspacesHelper, type: :helper do - pending "add some examples to (or delete) #{__FILE__}" -end diff --git a/spec/models/client_spec.rb b/spec/models/client_spec.rb index 1499253bff..69db876c89 100644 --- a/spec/models/client_spec.rb +++ b/spec/models/client_spec.rb @@ -89,9 +89,8 @@ let(:client) { create(:client, company:) } let(:project_1) { create(:project, client:) } let(:project_2) { create(:project, client:) } - let(:date_range_class) { Class.new { extend UtilityFunctions } } let(:results) do - range = date_range_class.range_from_timeframe(time_frame) + range = DateRangeService.new(timeframe: time_frame).process [project_1, project_2].map do | project | { id: project.id, name: project.name, billable: project.billable, team: project.project_member_full_names, From 8e5c76f1b097e7ddab274052420779ab3737d873 Mon Sep 17 00:00:00 2001 From: Prasanth Chaduvula Date: Tue, 22 Nov 2022 09:21:16 +0530 Subject: [PATCH 07/55] Fixed DateRange service bug (#794) * Fixed DateRange service :to attribute bug * Fixed DateRange service custom range bug --- app/services/date_range_service.rb | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/app/services/date_range_service.rb b/app/services/date_range_service.rb index 113514cf8d..12c41f5c6d 100644 --- a/app/services/date_range_service.rb +++ b/app/services/date_range_service.rb @@ -1,7 +1,13 @@ # frozen_string_literal: true class DateRangeService - attr_reader :timeframe, :from, to + attr_reader :timeframe, :from, :to + + class InvalidDatePassed < StandardError + def message + "Date value passed is invalid" + end + end def initialize(timeframe:, from: nil, to: nil) @timeframe = timeframe @@ -29,4 +35,12 @@ def process 0.year.ago.beginning_of_year..0.year.ago.end_of_year end end + + private + + def convert_to_date(value) + raise InvalidDatePassed if value.nil? + + Date.parse(value) + end end From 775f6425cd2bdc7363b98c4fe14027bb031413ad Mon Sep 17 00:00:00 2001 From: Apoorv Tiwari Date: Tue, 22 Nov 2022 14:07:46 +0530 Subject: [PATCH 08/55] specs for reference input (#774) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: “Apoorv <“tiwari.apoorv1316@gmail.com”> Co-authored-by: Mohini Dahiya --- .../Invoices/common/InvoiceDetails/index.tsx | 1 + cypress/constants/selectors/invoices.js | 3 ++- cypress/e2e/invoices/invoice.spec.js | 22 +++++++++++++++++-- cypress/fixtures/fake.js | 10 +++++++++ cypress/support/commands.js | 3 ++- 5 files changed, 35 insertions(+), 4 deletions(-) diff --git a/app/javascript/src/components/Invoices/common/InvoiceDetails/index.tsx b/app/javascript/src/components/Invoices/common/InvoiceDetails/index.tsx index 9595ea485e..83735cf132 100644 --- a/app/javascript/src/components/Invoices/common/InvoiceDetails/index.tsx +++ b/app/javascript/src/components/Invoices/common/InvoiceDetails/index.tsx @@ -96,6 +96,7 @@ const InvoiceDetails = ({ value={reference} onChange={(e) => setReference(e.target.value)} className="px-2 w-3/5" + data-cy="invoice-reference" />
diff --git a/cypress/constants/selectors/invoices.js b/cypress/constants/selectors/invoices.js index 08bc8adfb7..bce0868c24 100644 --- a/cypress/constants/selectors/invoices.js +++ b/cypress/constants/selectors/invoices.js @@ -16,5 +16,6 @@ export const invoicesSelector = { edit: dataCy('edit-invoice'), editNewLineItem: dataCy('edit-new-line-item'), saveInvoiceEdit: dataCy('save-invoice-edit'), - entriesListEdit: dataCy('entries-list-edit') + entriesListEdit: dataCy('entries-list-edit'), + referenceInput:dataCy('invoice-reference') } diff --git a/cypress/e2e/invoices/invoice.spec.js b/cypress/e2e/invoices/invoice.spec.js index 75b81c6d16..488a392d57 100644 --- a/cypress/e2e/invoices/invoice.spec.js +++ b/cypress/e2e/invoices/invoice.spec.js @@ -24,7 +24,8 @@ describe("invoices index page", () => { it("should generate an invoice and save it as a draft", function (){ const invoice_number = fake.invoiceNumber - cy.generateNewInvoice(invoice_number); + const reference = fake.validReference + cy.generateNewInvoice(invoice_number, reference); cy.get(invoicesSelector.invoicesList).first().contains(invoice_number); }) @@ -34,12 +35,28 @@ describe("invoices index page", () => { cy.contains("Please select client and enter invoice number to proceed"); }) + it("should throw an error when reference is greater than 12 characters", function(){ + const invoice_number = fake.invoiceNumber + const reference = fake.invalidReference + cy.get(invoicesSelector.newInvoiceButton).click() + cy.get(invoicesSelector.addClientButton).click() + cy.contains("Flipkart").click() + cy.get(invoicesSelector.invoiceNumberField).click().type(invoice_number) + cy.get(invoicesSelector.referenceInput).click().type(reference) + cy.get(invoicesSelector.newLineItemButton).click() + cy.get(invoicesSelector.entriesList).first().click({force: true}) + cy.get(invoicesSelector.saveInvoice).click() + cy.contains("Reference is too long (maximum is 12 characters)") + }) + it("should generate an invoice and send email", function (){ const invoice_number = fake.invoiceNumber + const reference = fake.validReference cy.get(invoicesSelector.newInvoiceButton).click() cy.get(invoicesSelector.addClientButton).click() cy.contains("Flipkart").click() cy.get(invoicesSelector.invoiceNumberField).click().type(invoice_number) + cy.get(invoicesSelector.referenceInput).click().type(reference) cy.get(invoicesSelector.newLineItemButton).click() cy.get(invoicesSelector.entriesList).first().click({force: true}) cy.get(invoicesSelector.sendInvoice).click({force: true}) @@ -48,7 +65,8 @@ describe("invoices index page", () => { it("should edit an invoice", function(){ const invoice_number = fake.invoiceNumber - cy.generateNewInvoice(invoice_number); + const reference = fake.validReference + cy.generateNewInvoice(invoice_number, reference); cy.get(invoicesSelector.searchBar).clear().type(invoice_number).type('{enter}') cy.get(invoicesSelector.edit).click({force: true}) cy.get(invoicesSelector.newLineItemButton).click() diff --git a/cypress/fixtures/fake.js b/cypress/fixtures/fake.js index 952df539ba..aeae043b4a 100644 --- a/cypress/fixtures/fake.js +++ b/cypress/fixtures/fake.js @@ -16,7 +16,17 @@ function invoiceNumber() { return faker.random.alphaNumeric(5); } +function invalidReference(){ + return faker.random.alphaNumeric(13) +} +function validReference(){ + return faker.random.alphaNumeric(6) +} + Object.defineProperty(fake, "firstName", { get: firstName }); Object.defineProperty(fake, "lastName", { get: lastName }); Object.defineProperty(fake, "email", { get: email }); Object.defineProperty(fake,'invoiceNumber',{get: invoiceNumber}); +Object.defineProperty(fake,'invalidReference',{get: invalidReference}); +Object.defineProperty(fake,'validReference',{get: validReference}); + diff --git a/cypress/support/commands.js b/cypress/support/commands.js index 8f56d1a845..7456d5e407 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -30,11 +30,12 @@ Cypress.Commands.add("loginAsEmployee", function () { cy.location("pathname").should("eq", "/time-tracking"); }); -Cypress.Commands.add("generateNewInvoice", function(invoice_number){ +Cypress.Commands.add("generateNewInvoice", function(invoice_number, reference){ cy.get(invoicesSelector.newInvoiceButton).click() cy.get(invoicesSelector.addClientButton).click() cy.contains("Flipkart").click() cy.get(invoicesSelector.invoiceNumberField).click().type(invoice_number) + cy.get(invoicesSelector.referenceInput).click().type(reference) cy.get(invoicesSelector.newLineItemButton).click() cy.get(invoicesSelector.entriesList).first().click({force: true}) cy.get(invoicesSelector.saveInvoice).click() From 10ef397d8e7928a2fbc200cc6282d31f913800eb Mon Sep 17 00:00:00 2001 From: Prasanth Chaduvula Date: Fri, 25 Nov 2022 07:37:52 +0530 Subject: [PATCH 09/55] Fix: can edit non billed entries (#784) * Can edit non billed entries * Fixed the params whitelisting on the update timesheet entry * Added test cases to timesheet entry --- .../v1/timesheet_entry_controller.rb | 7 +--- spec/models/timesheet_entry_spec.rb | 28 ++++++++++++- .../v1/timesheet_entries/update_spec.rb | 40 +++++++++++++++++++ 3 files changed, 68 insertions(+), 7 deletions(-) diff --git a/app/controllers/internal_api/v1/timesheet_entry_controller.rb b/app/controllers/internal_api/v1/timesheet_entry_controller.rb index a4cfbcbc22..721956eb5f 100644 --- a/app/controllers/internal_api/v1/timesheet_entry_controller.rb +++ b/app/controllers/internal_api/v1/timesheet_entry_controller.rb @@ -30,8 +30,9 @@ def create def update authorize current_timesheet_entry current_timesheet_entry.project = current_project + current_timesheet_entry.update!(timesheet_entry_params) render json: { notice: I18n.t("timesheet_entry.update.message"), entry: current_timesheet_entry.formatted_entry }, - status: :ok if current_timesheet_entry.update(timesheet_entry_update_params) + status: :ok end def destroy @@ -52,8 +53,4 @@ def current_timesheet_entry def timesheet_entry_params params.require(:timesheet_entry).permit(:project_id, :duration, :work_date, :note, :bill_status) end - - def timesheet_entry_update_params - params.require(:timesheet_entry).permit(:project_id, :duration, :work_date, :note) - end end diff --git a/spec/models/timesheet_entry_spec.rb b/spec/models/timesheet_entry_spec.rb index 53fefff36c..fc250124a0 100644 --- a/spec/models/timesheet_entry_spec.rb +++ b/spec/models/timesheet_entry_spec.rb @@ -117,15 +117,39 @@ timesheet_entry.update(bill_status: "unbilled") expect(timesheet_entry.valid?).to be_truthy + expect(timesheet_entry.bill_status).to eq("unbilled") expect(timesheet_entry.errors.blank?).to be true end end - context "when time entry is not billed" do - it "allows owners and admins to edit the billed time entry" do + context "when time entry is non billable" do + before do + timesheet_entry.update!(bill_status: "non_billable") + end + + it "allows owners and admins to edit the non billable time entry to unbilled" do + expect(timesheet_entry.bill_status).to eq("non_billable") + timesheet_entry.update(bill_status: "unbilled") expect(timesheet_entry.valid?).to be_truthy + expect(timesheet_entry.bill_status).to eq("unbilled") + expect(timesheet_entry.errors.blank?).to be true + end + end + + context "when time entry is unbilled" do + before do + timesheet_entry.update!(bill_status: "unbilled") + end + + it "allows owners and admins to edit the unbilled time entry to non billable" do + expect(timesheet_entry.bill_status).to eq("unbilled") + + timesheet_entry.update(bill_status: "non_billable") + + expect(timesheet_entry.valid?).to be_truthy + expect(timesheet_entry.bill_status).to eq("non_billable") expect(timesheet_entry.errors.blank?).to be true end end diff --git a/spec/requests/internal_api/v1/timesheet_entries/update_spec.rb b/spec/requests/internal_api/v1/timesheet_entries/update_spec.rb index d57026ea5d..652ad0bf41 100644 --- a/spec/requests/internal_api/v1/timesheet_entries/update_spec.rb +++ b/spec/requests/internal_api/v1/timesheet_entries/update_spec.rb @@ -46,6 +46,26 @@ expect(json_response["entry"]["bill_status"]).to match("billed") expect(json_response["notice"]).to match("Timesheet updated") end + + context "when time entry record is billed one" do + before do + timesheet_entry.update!(bill_status: "billed") + end + + it "they should be able to update billed time entry record to unbiiled successfully" do + expect(timesheet_entry.bill_status).to eq("billed") + + send_request :patch, internal_api_v1_timesheet_entry_path(timesheet_entry.id), params: { + project_id: project.id, + timesheet_entry: { + bill_status: :unbilled + } + } + + expect(response).to be_successful + expect(json_response["entry"]["bill_status"]).to match("unbilled") + end + end end context "when user is an employee" do @@ -74,6 +94,26 @@ expect(json_response["entry"]["bill_status"]).to match("billed") expect(json_response["notice"]).to match("Timesheet updated") end + + context "when time entry record is billed one" do + before do + timesheet_entry.update!(bill_status: "billed") + end + + it "they should not be able to update billed time entry record" do + expect(timesheet_entry.bill_status).to eq("billed") + + send_request :patch, internal_api_v1_timesheet_entry_path(timesheet_entry.id), params: { + project_id: project.id, + timesheet_entry: { + bill_status: :unbilled + } + } + + expect(response).to have_http_status(:forbidden) + expect(json_response["errors"]).to include("You are not authorized to perform this action.") + end + end end context "when employee tries to update other user's timesheet entry" do From f6307238a44f46152fb06fef91a626d1855d183f Mon Sep 17 00:00:00 2001 From: Shalaka Patil Date: Fri, 25 Nov 2022 17:38:44 +0530 Subject: [PATCH 10/55] Fix outstanding reports spec (#803) --- app/models/client.rb | 4 ++- .../index_spec.rb | 36 +++++++++---------- 2 files changed, 21 insertions(+), 19 deletions(-) diff --git a/app/models/client.rb b/app/models/client.rb index ab16b3b714..d706107081 100644 --- a/app/models/client.rb +++ b/app/models/client.rb @@ -115,7 +115,9 @@ def payment_summary(duration) def outstanding_and_overdue_invoices outstanding_overdue_statuses = ["overdue", "sent", "viewed"] - filtered_invoices = invoices.select { |invoice| outstanding_overdue_statuses.include?(invoice.status) } + filtered_invoices = invoices + .order(updated_at: :desc) + .select { |invoice| outstanding_overdue_statuses.include?(invoice.status) } status_and_amount = invoices.group(:status).sum(:amount) status_and_amount.default = 0 diff --git a/spec/requests/internal_api/v1/reports/outstanding_overdue_invoices/index_spec.rb b/spec/requests/internal_api/v1/reports/outstanding_overdue_invoices/index_spec.rb index fa57718793..54e22adabd 100644 --- a/spec/requests/internal_api/v1/reports/outstanding_overdue_invoices/index_spec.rb +++ b/spec/requests/internal_api/v1/reports/outstanding_overdue_invoices/index_spec.rb @@ -34,14 +34,6 @@ totalOutstandingAmount: @client2_outstanding_amount, totalOverdueAmount: @client2_overdue_amount, invoices: [ - { - clientName: client2.name, - invoiceNo: client2_sent_invoice1.invoice_number, - issueDate: client2_sent_invoice1.issue_date, - dueDate: client2_sent_invoice1.due_date, - amount: client2_sent_invoice1.amount, - status: client2_sent_invoice1.status - }, { clientName: client2.name, invoiceNo: client2_overdue_invoice1.invoice_number, @@ -49,6 +41,14 @@ dueDate: client2_overdue_invoice1.due_date, amount: client2_overdue_invoice1.amount, status: client2_overdue_invoice1.status + }, + { + clientName: client2.name, + invoiceNo: client2_sent_invoice1.invoice_number, + issueDate: client2_sent_invoice1.issue_date, + dueDate: client2_sent_invoice1.due_date, + amount: client2_sent_invoice1.amount, + status: client2_sent_invoice1.status } ] }, @@ -59,11 +59,11 @@ invoices: [ { clientName: client1.name, - invoiceNo: client1_sent_invoice1.invoice_number, - issueDate: client1_sent_invoice1.issue_date, - dueDate: client1_sent_invoice1.due_date, - amount: client1_sent_invoice1.amount, - status: client1_sent_invoice1.status + invoiceNo: client1_viewed_invoice1.invoice_number, + issueDate: client1_viewed_invoice1.issue_date, + dueDate: client1_viewed_invoice1.due_date, + amount: client1_viewed_invoice1.amount, + status: client1_viewed_invoice1.status }, { clientName: client1.name, @@ -75,11 +75,11 @@ }, { clientName: client1.name, - invoiceNo: client1_viewed_invoice1.invoice_number, - issueDate: client1_viewed_invoice1.issue_date, - dueDate: client1_viewed_invoice1.due_date, - amount: client1_viewed_invoice1.amount, - status: client1_viewed_invoice1.status + invoiceNo: client1_sent_invoice1.invoice_number, + issueDate: client1_sent_invoice1.issue_date, + dueDate: client1_sent_invoice1.due_date, + amount: client1_sent_invoice1.amount, + status: client1_sent_invoice1.status } ] }] From 69f98de49786684f7539a49031c39ba483f8d190 Mon Sep 17 00:00:00 2001 From: Prasanth Chaduvula Date: Tue, 29 Nov 2022 23:04:56 +0530 Subject: [PATCH 11/55] Linting and formating code (#796) * Added prepush and prevent push hooks * Added prettier config * Added eslint config * Added eslint config * Added eslint config * Added eslint config * Added vscode settings * Updated rubocop config * Added eslint config * Updated rubocop config * Added erb linting config * Refactored & formatted components with new lintings rules * Updated eslint config * Refactored & formatted components with new lintings rules * Updated rubocop config --- .erb-lint.yml | 20 + .eslint-rules/custom.js | 1 + .eslint-rules/globals.js | 13 + .eslint-rules/helpers/index.js | 49 + .eslint-rules/imports/enforced.js | 29 + .eslint-rules/imports/order.js | 64 + .eslint-rules/overrides.js | 24 + .eslint-rules/promise.js | 8 + .eslint-rules/react.js | 92 ++ .eslintignore | 16 + .eslintrc | 253 ++-- .husky/helpers/prevent_pushing_to_develop.sh | 24 + .husky/pre-commit | 2 + .husky/pre-push | 5 + .prettierrc.js | 16 + .rubocop.yml | 54 +- .vscode/extensions.json | 13 + .vscode/settings.json | 36 +- Gemfile | 6 +- Gemfile.lock | 27 +- .../src/StyledComponents/Avatar.tsx | 22 +- app/javascript/src/StyledComponents/Badge.tsx | 15 +- .../src/StyledComponents/Button.tsx | 9 +- .../src/StyledComponents/SidePanel/Footer.tsx | 2 +- .../src/StyledComponents/SidePanel/index.tsx | 4 +- .../src/StyledComponents/TimeInput/index.tsx | 52 +- .../StyledComponents/TimeInput/validate.ts | 26 +- .../src/StyledComponents/Tooltip.tsx | 6 +- app/javascript/src/StyledComponents/index.tsx | 11 +- app/javascript/src/apis/axios.ts | 20 +- app/javascript/src/apis/clients.ts | 4 +- app/javascript/src/apis/companies.ts | 2 +- app/javascript/src/apis/generateInvoice.ts | 2 +- app/javascript/src/apis/invoices.ts | 27 +- app/javascript/src/apis/payments/payments.ts | 2 +- app/javascript/src/apis/payments/providers.ts | 2 +- app/javascript/src/apis/profile.ts | 4 +- app/javascript/src/apis/profiles.ts | 2 +- app/javascript/src/apis/project-members.ts | 2 +- app/javascript/src/apis/projects.ts | 5 +- app/javascript/src/apis/reports.ts | 13 +- .../src/apis/reports/clientRevenue.ts | 5 +- app/javascript/src/apis/team.ts | 16 +- app/javascript/src/apis/timesheet-entry.ts | 13 +- app/javascript/src/apis/wise.ts | 26 +- app/javascript/src/common/AmountBox/index.tsx | 13 +- app/javascript/src/common/AutoComplete.tsx | 74 +- app/javascript/src/common/ChartBar/index.tsx | 63 +- .../src/common/ChartBar/interface.ts | 2 +- app/javascript/src/common/CustomCheckbox.tsx | 42 +- .../src/common/CustomDatePicker/index.tsx | 21 +- .../common/CustomDateRangePicker/index.tsx | 86 +- app/javascript/src/common/CustomRadio.tsx | 31 +- app/javascript/src/common/CustomToggle.tsx | 10 +- app/javascript/src/common/Divider/index.tsx | 3 +- app/javascript/src/common/Error/index.tsx | 4 +- app/javascript/src/common/Loader.tsx | 16 +- app/javascript/src/common/Loader/index.tsx | 43 +- app/javascript/src/common/Pagination.tsx | 37 +- .../src/common/ProgressBar/index.tsx | 2 +- .../src/common/SearchTimeEntries.tsx | 36 +- app/javascript/src/common/Table/index.tsx | 186 +-- app/javascript/src/common/Toastr.tsx | 28 +- .../src/common/TotalHeader/index.tsx | 13 +- app/javascript/src/components/App.tsx | 27 +- .../src/components/Clients/Details/Header.tsx | 140 +- .../src/components/Clients/Details/index.tsx | 198 +-- .../src/components/Clients/List/Header.tsx | 19 +- .../src/components/Clients/List/index.tsx | 207 +-- .../components/Clients/Modals/AddProject.tsx | 107 +- .../Clients/Modals/DeleteClient.tsx | 18 +- .../components/Clients/Modals/EditClient.tsx | 126 +- .../components/Clients/Modals/NewClient.tsx | 107 +- .../src/components/Clients/interface.ts | 2 +- .../components/EmailVerification/index.tsx | 22 +- .../src/components/InvoiceEmail/Header.tsx | 28 +- .../InvoiceEmail/InvoiceDetails.tsx | 17 +- .../components/InvoiceEmail/InvoiceInfo.tsx | 44 +- .../InvoiceEmail/InvoiceTotalSummary.tsx | 64 +- .../components/InvoiceEmail/PayOnlineMenu.tsx | 16 +- .../src/components/InvoiceEmail/index.tsx | 20 +- .../src/components/Invoices/Edit/Header.tsx | 20 +- .../src/components/Invoices/Edit/index.tsx | 136 +- .../Invoices/Generate/Container.tsx | 77 +- .../Invoices/Generate/InvoiceSettings.tsx | 224 ++-- .../components/Invoices/Generate/index.tsx | 127 +- .../Invoices/Invoice/BackButton.tsx | 2 +- .../Invoices/Invoice/ClientInfo.tsx | 9 +- .../Invoices/Invoice/DeleteButton.tsx | 12 +- .../Invoices/Invoice/EditButton.tsx | 12 +- .../components/Invoices/Invoice/Header.tsx | 32 +- .../Invoices/Invoice/InvoiceActions.tsx | 36 +- .../Invoices/Invoice/InvoiceDetails.tsx | 8 +- .../Invoices/Invoice/InvoiceInfo.tsx | 29 +- .../Invoices/Invoice/InvoiceLineItems.tsx | 32 +- .../Invoices/Invoice/InvoiceStatus.tsx | 6 +- .../Invoices/Invoice/InvoiceTotalSummary.tsx | 66 +- .../components/Invoices/Invoice/LineItem.tsx | 18 +- .../Invoices/Invoice/SendButton.tsx | 12 +- .../src/components/Invoices/Invoice/index.tsx | 41 +- .../Invoices/InvoiceSummary/index.tsx | 36 +- .../List/FilterSideBar/filterOptions.ts | 66 +- .../Invoices/List/FilterSideBar/index.tsx | 219 +-- .../src/components/Invoices/List/Header.tsx | 102 +- .../List/InvoiceSearch/SearchDropdown.tsx | 21 +- .../List/InvoiceSearch/SearchedDataRow.tsx | 16 +- .../components/Invoices/List/MoreOptions.tsx | 20 +- .../Invoices/List/RecentlyUpdated/index.tsx | 14 +- .../Invoices/List/SendInvoice/index.tsx | 136 +- .../Invoices/List/SendInvoice/utils.ts | 6 +- .../Invoices/List/Table/TableHeader.tsx | 34 +- .../Invoices/List/Table/TableRow.tsx | 66 +- .../components/Invoices/List/Table/index.tsx | 27 +- .../components/Invoices/List/container.tsx | 149 ++- .../src/components/Invoices/List/index.tsx | 119 +- .../MultipleEntriesModal/FilterSelect.tsx | 14 +- .../Filters/DateRange.tsx | 69 +- .../Filters/SearchTeamMembers.tsx | 23 +- .../MultipleEntriesModal/Filters/index.tsx | 189 ++- .../Invoices/MultipleEntriesModal/Footer.tsx | 32 +- .../Invoices/MultipleEntriesModal/Header.tsx | 20 +- .../Invoices/MultipleEntriesModal/Table.tsx | 64 +- .../Invoices/MultipleEntriesModal/index.tsx | 92 +- .../Invoices/common/CompanyInfo/index.tsx | 20 +- .../common/InvoiceDetails/ClientSelection.tsx | 59 +- .../Invoices/common/InvoiceDetails/Styles.ts | 29 +- .../Invoices/common/InvoiceDetails/index.tsx | 79 +- .../common/InvoiceForm/Header/index.tsx | 67 +- .../common/InvoiceForm/SendInvoice/index.tsx | 147 +- .../common/InvoiceForm/SendInvoice/utils.tsx | 11 +- .../Invoices/common/InvoiceTable/index.tsx | 140 +- .../common/InvoiceTotal/DiscountMenu.tsx | 25 +- .../Invoices/common/InvoiceTotal/index.tsx | 166 ++- .../common/LineItemTableHeader/index.tsx | 26 +- .../Invoices/common/ManualEntry/index.tsx | 64 +- .../components/Invoices/common/MoreButton.tsx | 4 +- .../Invoices/common/MoreOptions.tsx | 24 +- .../common/NewLineItemRow/EditLineItems.tsx | 69 +- .../NewLineItemRow/NewLineItemStatic.tsx | 78 +- .../Invoices/common/NewLineItemRow/index.tsx | 28 +- .../common/NewLineItemTable/Header.tsx | 17 +- .../common/NewLineItemTable/index.tsx | 69 +- .../src/components/Invoices/common/utils.js | 35 +- .../Invoices/popups/BulkDeleteInvoices.tsx | 42 +- .../Invoices/popups/DeleteInvoice.tsx | 42 +- .../Invoices/popups/SendInvoice/index.tsx | 134 +- .../Invoices/popups/SendInvoice/utils.tsx | 11 +- app/javascript/src/components/Main.tsx | 37 +- .../src/components/Navbar/index.tsx | 100 +- .../src/components/PlanDetails/index.tsx | 65 +- .../BankAccountDetails/AddressDetails.tsx | 104 +- .../BankAccountDetails/BankDetails.tsx | 124 +- .../Profile/BankAccountDetails/BankInfo.tsx | 27 +- .../BankAccountDetails/BillingDetailInput.tsx | 65 +- .../BankAccountDetails/CurrencyDropdown.tsx | 40 +- .../Profile/BankAccountDetails/index.tsx | 122 +- .../Profile/Billing/Table/TableHeader.tsx | 27 +- .../Profile/Billing/Table/TableRow.tsx | 65 +- .../Profile/Billing/Table/index.tsx | 13 +- .../src/components/Profile/Billing/index.tsx | 78 +- .../src/components/Profile/Header.tsx | 71 +- .../src/components/Profile/Layout.tsx | 62 +- .../Billing/Table/TableHeader.tsx | 27 +- .../Organization/Billing/Table/TableRow.tsx | 65 +- .../Organization/Billing/Table/index.tsx | 13 +- .../Profile/Organization/Billing/index.tsx | 82 +- .../Profile/Organization/Edit/index.tsx | 489 ++++--- .../Profile/Organization/Import/Styles.ts | 27 +- .../Organization/Import/TableHeader.tsx | 7 +- .../Profile/Organization/Import/TableRow.tsx | 56 +- .../Organization/Import/importCard.tsx | 36 +- .../Organization/Import/importModal.tsx | 228 ++-- .../Profile/Organization/Import/index.tsx | 98 +- .../Profile/Organization/Payment/index.tsx | 70 +- .../src/components/Profile/RouteConfig.tsx | 17 +- .../src/components/Profile/SubNav.tsx | 72 +- .../components/Profile/UserDetail/index.tsx | 396 +++--- .../Profile/context/EntryContext.tsx | 6 +- .../Projects/Details/EditMembersList.tsx | 80 +- .../Projects/Details/EditMembersListForm.tsx | 94 +- .../src/components/Projects/Details/index.tsx | 146 +- .../src/components/Projects/List/Header.tsx | 56 +- .../src/components/Projects/List/index.tsx | 55 +- .../src/components/Projects/List/project.tsx | 82 +- .../Projects/Modals/AddEditProject.tsx | 155 ++- .../Projects/Modals/DeleteProject.tsx | 23 +- .../src/components/Projects/interface.ts | 2 +- .../Reports/Container/ReportRow.tsx | 23 +- .../components/Reports/Container/index.tsx | 30 +- .../Reports/Filters/filterOptions.ts | 50 +- .../src/components/Reports/Filters/index.tsx | 160 ++- .../src/components/Reports/Filters/style.ts | 10 +- .../Reports/Header/NavigationFilter.tsx | 60 +- .../components/Reports/Header/fetchReport.tsx | 9 +- .../src/components/Reports/Header/index.tsx | 98 +- .../src/components/Reports/api/applyFilter.ts | 32 +- .../Reports/api/outstandingOverdueInvoice.ts | 12 +- .../components/Reports/api/revenueByClient.ts | 41 +- .../Reports/context/EntryContext.tsx | 6 +- .../context/RevenueByClientContext.tsx | 12 +- .../context/TimeEntryReportContext.tsx | 8 +- .../outstandingOverdueInvoiceContext.tsx | 12 +- .../Container/TableRow.tsx | 34 +- .../outstandingInvoices/Container/index.tsx | 91 +- .../Filters/filterOptions.ts | 21 +- .../outstandingInvoices/Filters/index.tsx | 114 +- .../outstandingInvoices/Filters/style.ts | 10 +- .../Reports/outstandingInvoices/index.tsx | 103 +- .../Reports/outstandingInvoices/interface.ts | 18 +- .../components/Reports/reportList/index.tsx | 85 +- .../Reports/reportList/reportCard.tsx | 17 +- .../revenueByClient/Container/TableRow.tsx | 23 +- .../revenueByClient/Container/index.tsx | 47 +- .../revenueByClient/Filters/filterOptions.ts | 22 +- .../Reports/revenueByClient/Filters/index.tsx | 126 +- .../Reports/revenueByClient/Filters/style.ts | 10 +- .../Reports/revenueByClient/index.tsx | 93 +- .../Reports/revenueByClient/interface.ts | 8 +- .../components/Reports/timeEntry/index.tsx | 83 +- .../Reports/totalHoursLogged/index.tsx | 18 +- .../Subscriptions/PlanSelection/index.tsx | 99 +- .../CompensationDetails/StaticPage.tsx | 21 +- .../Details/CompensationDetails/index.tsx | 4 +- .../Team/Details/DeviceDetails/StaticPage.tsx | 34 +- .../Team/Details/DeviceDetails/index.tsx | 4 +- .../Team/Details/DocumentDetails/index.tsx | 6 +- .../Details/EmploymentDetails/StaticPage.tsx | 50 +- .../Team/Details/EmploymentDetails/index.tsx | 16 +- .../components/Team/Details/Layout/Header.tsx | 10 +- .../Team/Details/Layout/OutletWrapper.tsx | 2 +- .../Team/Details/Layout/SideNav.tsx | 64 +- .../Details/PersonalDetails/StaticPage.tsx | 46 +- .../Team/Details/PersonalDetails/index.tsx | 9 +- .../ReimburstmentDetails/StaticPage.tsx | 52 +- .../Details/ReimburstmentDetails/index.tsx | 4 +- .../src/components/Team/Details/index.tsx | 15 +- .../src/components/Team/List/Header.tsx | 11 +- .../components/Team/List/Table/TableHead.tsx | 19 +- .../components/Team/List/Table/TableRow.tsx | 64 +- .../src/components/Team/List/Table/index.tsx | 9 +- .../src/components/Team/List/index.tsx | 46 +- .../src/components/Team/RouteConfig.tsx | 8 +- .../components/Team/modals/AddEditMember.tsx | 103 +- .../components/Team/modals/DeleteMember.tsx | 34 +- .../src/components/Team/modals/Modals.tsx | 3 +- .../src/components/TimeTracking/AddEntry.tsx | 173 +-- .../components/TimeTracking/DatesInWeek.tsx | 33 +- .../src/components/TimeTracking/EntryCard.tsx | 55 +- .../components/TimeTracking/MonthCalender.tsx | 229 ++-- .../components/TimeTracking/SelectProject.tsx | 47 +- .../components/TimeTracking/WeeklyEntries.tsx | 68 +- .../TimeTracking/WeeklyEntriesCard.tsx | 133 +- .../src/components/TimeTracking/index.tsx | 301 +++-- .../src/components/payments/Header.tsx | 10 +- .../payments/Modals/AddManualEntry.tsx | 134 +- .../components/payments/Table/TableHeader.tsx | 26 +- .../components/payments/Table/TableRow.tsx | 38 +- .../src/components/payments/Table/index.tsx | 12 +- .../src/components/payments/index.tsx | 25 +- app/javascript/src/constants/countryList.ts | 2 +- app/javascript/src/constants/currencyList.ts | 1192 ++++++++--------- app/javascript/src/constants/index.tsx | 69 +- app/javascript/src/constants/routes.ts | 109 +- app/javascript/src/context/TeamContext.tsx | 8 +- .../src/context/TeamDetailsContext.tsx | 8 +- app/javascript/src/context/UserContext.tsx | 6 +- .../src/helpers/byteToSizeConverter.ts | 5 +- app/javascript/src/helpers/cashFormater.ts | 3 +- app/javascript/src/helpers/currency.ts | 6 +- app/javascript/src/helpers/currencySymbol.ts | 2 +- app/javascript/src/helpers/dateParser.ts | 5 +- app/javascript/src/helpers/debounce.ts | 1 + app/javascript/src/helpers/hhmmParser.ts | 20 +- app/javascript/src/helpers/index.ts | 7 +- app/javascript/src/helpers/lineTotalCalc.ts | 3 +- app/javascript/src/helpers/ordinal.ts | 1 + app/javascript/src/helpers/outsideClick.ts | 9 +- .../src/helpers/validateTimesheetEntry.ts | 3 +- .../src/helpers/wiseUtilityFunctions.ts | 18 +- app/javascript/src/mapper/client.mapper.ts | 55 +- .../src/mapper/editInvoice.mapper.ts | 11 +- .../src/mapper/generateInvoice.mapper.ts | 33 +- app/javascript/src/mapper/payment.mapper.ts | 13 +- app/javascript/src/mapper/project.mapper.ts | 20 +- app/javascript/src/mapper/report.mapper.ts | 14 +- app/javascript/src/mapper/team.mapper.ts | 15 +- app/javascript/src/miruIcons/index.ts | 2 +- app/javascript/src/utils/dateUtil.ts | 30 +- app/javascript/src/utils/getBadgeStatus.ts | 5 +- package.json | 24 +- yarn.lock | 10 + 291 files changed, 8733 insertions(+), 6596 deletions(-) create mode 100644 .erb-lint.yml create mode 100644 .eslint-rules/custom.js create mode 100644 .eslint-rules/globals.js create mode 100644 .eslint-rules/helpers/index.js create mode 100644 .eslint-rules/imports/enforced.js create mode 100644 .eslint-rules/imports/order.js create mode 100644 .eslint-rules/overrides.js create mode 100644 .eslint-rules/promise.js create mode 100644 .eslint-rules/react.js create mode 100644 .eslintignore create mode 100644 .husky/helpers/prevent_pushing_to_develop.sh create mode 100644 .husky/pre-push create mode 100644 .prettierrc.js create mode 100644 .vscode/extensions.json diff --git a/.erb-lint.yml b/.erb-lint.yml new file mode 100644 index 0000000000..0524feab8b --- /dev/null +++ b/.erb-lint.yml @@ -0,0 +1,20 @@ +--- +glob: "app/views/**/*.{html}{+*,}.erb" +exclude: + - "**/vendor/**/*" + - "**/node_modules/**/*" +EnableDefaultLinters: true +linters: + PartialInstanceVariable: + enabled: true + ErbSafety: + enabled: true + Rubocop: + enabled: true + rubocop_config: + inherit_from: + - .rubocop.yml + Style/FrozenStringLiteralComment: + Enabled: false + Layout/TrailingEmptyLines: + Enabled: false diff --git a/.eslint-rules/custom.js b/.eslint-rules/custom.js new file mode 100644 index 0000000000..f053ebf797 --- /dev/null +++ b/.eslint-rules/custom.js @@ -0,0 +1 @@ +module.exports = {}; diff --git a/.eslint-rules/globals.js b/.eslint-rules/globals.js new file mode 100644 index 0000000000..cb0884a705 --- /dev/null +++ b/.eslint-rules/globals.js @@ -0,0 +1,13 @@ +module.exports = { + // Globals can be disabled with the string "off" + // "writable" to allow the variable to be overwritten or "readonly" to disallow overwriting. + globals: { + Atomics: "readonly", + SharedArrayBuffer: "readonly", + // Makes logger function available everywhere. Else eslint will complaint of undef-var. + logger: "readonly", + module: "writable", + // Makes props obtained from Rails backend available everywhere in this project. + globalProps: "readonly", + }, +}; diff --git a/.eslint-rules/helpers/index.js b/.eslint-rules/helpers/index.js new file mode 100644 index 0000000000..d3b487285e --- /dev/null +++ b/.eslint-rules/helpers/index.js @@ -0,0 +1,49 @@ +const fs = require("fs"); + +const buildPathGroupsBasedOnWebpackAliases = ({ + customJSRoot = "app/javascript/", + customAliasPath = "config/webpack/alias.js", +}) => { + const rootOfProject = __dirname + `/../../`; + + const isFile = filePath => + fs.existsSync(filePath) && fs.lstatSync(filePath).isFile(); + + const webpackAliasPath = rootOfProject + customAliasPath; + + const hasWebpackAliasConfig = isFile(webpackAliasPath); + + const isRailsProject = isFile(rootOfProject + "Gemfile"); + + const emptyPathGroups = []; + + if (!hasWebpackAliasConfig || !isRailsProject) return emptyPathGroups; + + const { + resolve: { alias }, + } = require(webpackAliasPath); + + const railsJSFilesRoot = rootOfProject + customJSRoot; + + const pathGroups = Object.entries(alias).map(([name, path]) => { + // sometimes alias might be already resolved to full absolute path + const isAleadyAnAbsolutePath = + path.includes("cypress-tests/") || path.includes("app/"); + + const absolutePath = isAleadyAnAbsolutePath + ? path + : `${railsJSFilesRoot}${path}`; + const wildCard = + isFile(absolutePath + ".js") || isFile(absolutePath + ".jsx") + ? "" + : "/**"; + + let group = "internal"; + + return { pattern: `${name}${wildCard}`, group }; + }); + + return pathGroups; +}; + +module.exports = { buildPathGroupsBasedOnWebpackAliases }; diff --git a/.eslint-rules/imports/enforced.js b/.eslint-rules/imports/enforced.js new file mode 100644 index 0000000000..1d4c169b38 --- /dev/null +++ b/.eslint-rules/imports/enforced.js @@ -0,0 +1,29 @@ +module.exports = { + rules: { + // not-auto-fixable: Prefer a default export if module exports a single name. + "import/prefer-default-export": "off", + // not-auto-fixable: Forbid a module from importing a module with a dependency path back to itself. + "import/no-cycle": ["error", { maxDepth: 1, ignoreExternal: true }], + // not-auto-fixable: Prevent unnecessary path segments in import and require statements. + "import/no-useless-path-segments": ["error", { noUselessIndex: true }], + // not-auto-fixable: Report any invalid exports, i.e. re-export of the same name. + "import/export": "error", + // not-auto-fixable: Forbid the use of mutable exports with var or let. + "import/no-mutable-exports": "error", + // not-auto-fixable: Ensure all imports appear before other statements. + "import/first": "error", + // not-auto-fixable: Ensure all exports appear after other statements. + "import/exports-last": "error", + // auto-fixable: Enforce a newline after import statements. + "import/newline-after-import": ["error", { count: 1 }], + // auto-fixable: Remove file extensions for import statements. + "import/extensions": [ + "error", + "never", + { + ignorePackages: true, + pattern: { json: "always", mp3: "always", svg: "always", mapper: "always" }, + }, + ], + }, +}; diff --git a/.eslint-rules/imports/order.js b/.eslint-rules/imports/order.js new file mode 100644 index 0000000000..4561d595fb --- /dev/null +++ b/.eslint-rules/imports/order.js @@ -0,0 +1,64 @@ +const { buildPathGroupsBasedOnWebpackAliases } = require(__dirname + + "/../helpers"); +const pathGroups = buildPathGroupsBasedOnWebpackAliases({}); + +const pathGroupForKeepingReactImportsAtTop = { + pattern: "react+(-native|)", + group: "external", + position: "before", +}; + +/* +Example pathGroups structure. Adding this here +so that if anyone wants to add custom config, +they can make use of this: +[ + { pattern: 'apis/**', group: 'internal' }, + { pattern: 'common/**', group: 'internal' }, + { pattern: 'components/**', group: 'internal' }, + { pattern: 'constants/**', group: 'internal' }, + { pattern: 'contexts/**', group: 'internal' }, + { pattern: 'reducers/**', group: 'internal' }, + { pattern: 'Constants', group: 'internal' }, + { + pattern: 'react+(-native|)', + group: 'external', + position: 'before' + } +] +*/ +pathGroups.push(pathGroupForKeepingReactImportsAtTop); + +module.exports = { + rules: { + // auto-fixable: Enforce a convention in module import order + "import/order": [ + "error", + { + "newlines-between": "always", + alphabetize: { order: "asc", caseInsensitive: true }, + warnOnUnassignedImports: true, + groups: [ + "builtin", + "external", + "internal", + "index", + "sibling", + "parent", + "object", + "type", + ], + /* + * Currently we check for existence of webpack alias + * config and then iterate over the aliases and create + * these pathGroups. Only caveat with this mechanism + * is that in VSCode eslint plugin won't dynamically + * read it. But eslint cli would! + */ + pathGroups, + // Ignore react imports so that they're always ordered to the top of the file. + pathGroupsExcludedImportTypes: ["react", "react-native"], + }, + ], + }, +}; diff --git a/.eslint-rules/overrides.js b/.eslint-rules/overrides.js new file mode 100644 index 0000000000..4399b54d61 --- /dev/null +++ b/.eslint-rules/overrides.js @@ -0,0 +1,24 @@ +module.exports = { + // Currently we are using this section for excluding certain files from certain rules. + overrides: [ + { + files: [ + ".eslintrc.js", + ".prettierrc.js", + "app/assets/**/*", + "app/javascript/packs/**/*", + "*.json", + ], + rules: { + "import/order": "off", + "react-hooks/rules-of-hooks": "off", + }, + }, + { + files: ["app/javascript/packs/**/*.{js,jsx}"], + rules: { + "no-redeclare": "off", + }, + }, + ], +}; diff --git a/.eslint-rules/promise.js b/.eslint-rules/promise.js new file mode 100644 index 0000000000..c9d59d9904 --- /dev/null +++ b/.eslint-rules/promise.js @@ -0,0 +1,8 @@ +module.exports = { + rules: { + // not-auto-fixable: ensure people use async/await promising chaining rather than using "then-catch-finally" statements + "promise/prefer-await-to-then": "error", + // auto-fixable: avoid calling "new" on a Promise static method like reject, resolve etc + "promise/no-new-statics": "error", + }, +}; diff --git a/.eslint-rules/react.js b/.eslint-rules/react.js new file mode 100644 index 0000000000..35a5b30fc8 --- /dev/null +++ b/.eslint-rules/react.js @@ -0,0 +1,92 @@ +module.exports = { + rules: { + // not-auto-fixable: Prevent missing props validation in a React component definition. + "react/prop-types": "off", + // not-auto-fixable: Detect unescaped HTML entities, which might represent malformed tags. + "react/no-unescaped-entities": "off", + // not-auto-fixable: Prevent missing displayName in a React component definition. Useful when using React extensions in browser and checking for component name. + "react/display-name": "error", + // not-auto-fixable: Reports when this.state is accessed within setState. + "react/no-access-state-in-setstate": "error", + // not-auto-fixable: Prevent usage of dangerous JSX props. Currently jam3 plugin will take care of handling this. + "react/no-danger": "off", + // not-auto-fixable: Report when a DOM element is using both children and dangerouslySetInnerHTML. + "react/no-danger-with-children": "warn", + // not-auto-fixable: Prevent definitions of unused prop types. + "react/no-unused-prop-types": "error", + // not-auto-fixable: Report missing key props in iterators/collection literals. Important rule! + "react/jsx-key": "error", + // not-auto-fixable: Enforce no duplicate props. + "react/jsx-no-duplicate-props": "error", + // not-auto-fixable: Disallow undeclared variables in JSX. + "react/jsx-no-undef": "error", + // not-auto-fixable: Enforce PascalCase for user-defined JSX components. + "react/jsx-pascal-case": ["error", { allowNamespace: true }], + // not-auto-fixable: Prevent React to be incorrectly marked as unused. + "react/jsx-uses-react": "error", + // not-auto-fixable: Prevent variables used in JSX to be marked as unused. + "react/jsx-uses-vars": "error", + // not-auto-fixable: Ensures https://reactjs.org/docs/hooks-rules.html. + "react-hooks/rules-of-hooks": "error", + // not-auto-fixable: Ensures https://reactjs.org/docs/hooks-rules.html - Checks effect dependencies. + "react-hooks/exhaustive-deps": "warn", + // auto-fixable: A fragment is redundant if it contains only one child, or if it is the child of a html element, and is not a keyed fragment. + "react/jsx-no-useless-fragment": ["error", { allowExpressions: true }], + // auto-fixable: Prefer arrow function expressions for component declaration. + "react/function-component-definition": [ + "error", + { + namedComponents: "arrow-function", + unnamedComponents: "arrow-function", + }, + ], + // auto-fixable: Components without children can be self-closed to avoid unnecessary extra closing tag. + "react/self-closing-comp": [ + "error", + { + component: true, + html: true, + }, + ], + // auto-fixable: Wrapping multiline JSX in parentheses can improve readability and/or convenience. + "react/jsx-wrap-multilines": [ + "error", + { + declaration: "parens-new-line", + assignment: "parens-new-line", + return: "parens-new-line", + arrow: "parens-new-line", + condition: "parens-new-line", + logical: "parens-new-line", + prop: "ignore", + }, + ], + // not-auto-fixable: Make sure files containing JSX is having .jsx extension. + "react/jsx-filename-extension": ["error", { allow: "as-needed" }], + // auto-fixable: Omit mentioning the "true" value if it can be implicitly understood in props. + "react/jsx-boolean-value": "error", + // auto-fixable: Partially fixable. Make sure the state and setter have symmertic naming. + "react/hook-use-state": "error", + // auto-fixable: Shorthand notations should always be at the top and also enforce props alphabetical sorting. + "react/jsx-sort-props": [ + "error", + { + callbacksLast: true, + shorthandFirst: true, + multiline: "last", + reservedFirst: false, + locale: "auto", + }, + ], + // auto-fixable: Disallow unnecessary curly braces in JSX props and/or children. + "react/jsx-curly-brace-presence": [ + "error", + { + props: "never", + children: "never", + // JSX prop values that are JSX elements should be enclosed in braces. + propElementValues: "always", + }, + ], + }, +}; diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000000..11279c58fc --- /dev/null +++ b/.eslintignore @@ -0,0 +1,16 @@ +node_modules +build +.eslintrc +public +coverage +db +docs +log +.scripts +test +tmp +.vscode +babel.config.js +app/javascript/packs +jsconfig.json +package.json diff --git a/.eslintrc b/.eslintrc index ff67d35757..01f8deb807 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,106 +1,163 @@ { - "root": true, - "parser": "@typescript-eslint/parser", - "parserOptions": { - "project": "./tsconfig.json", - "sourceType": "module" + "root": true, + "parser": "@typescript-eslint/parser", + "parserOptions": { + "project": "./tsconfig.json", + "sourceType": "module" + }, + "env": { + "es6": true, + "node": true, + "jest": true + }, + "extends": [ + "eslint:recommended", + "plugin:react/recommended", + "./.eslint-rules/globals", + "./.eslint-rules/imports/order", + "./.eslint-rules/overrides", + // ensure that you don't add custom rules + // without taking permission from team leads. + "./.eslint-rules/custom", + // custom rules cannot override the following rules. + "./.eslint-rules/imports/enforced", + "./.eslint-rules/react", + "./.eslint-rules/promise", + "plugin:@typescript-eslint/eslint-recommended", + "plugin:@typescript-eslint/recommended", + "prettier" + ], + "settings": { + "import/extensions": [".js", ".jsx", ".ts", ".tsx"], + "import/parsers": { + "@typescript-eslint/parser": [".ts", ".tsx"] }, - "env": { - "es6": true, - "node": true, - "jest": true + "import/resolver": { + "node": { + "extensions": [".js", ".jsx", ".ts", ".tsx", ".svg", ".json", ".mp3"] + } }, - "extends": [ - "eslint:recommended", - "plugin:react/recommended", - "plugin:@typescript-eslint/eslint-recommended", - "plugin:@typescript-eslint/recommended" + "react": { + "pragma": "React", + "fragment": "Fragment", + "version": "detect" + } + }, + "plugins": [ + "react", + "prettier", + "import", + "@typescript-eslint", + "react-hooks", + "promise", + "jam3", + "unused-imports" + ], + "globals": { + "fetch": false, + "document": false, + "Promise": true, + "log": true, + "sessionStorage": true, + "localStorage": true, + "FileReader": true, + "window": true + }, + "rules": { + "@typescript-eslint/no-explicit-any": "off", + // auto-fixable: Respect all Prettier rules and apply it. + "prettier/prettier": "error", + "react/jsx-filename-extension": [ + "error", + { "extensions": [".js",".jsx",".ts",".tsx"] } + ], + // not-auto-fixable: No unused variables allowed. + "no-unused-vars": [ + "error", + { + "args": "all", + "argsIgnorePattern": "^_", + "destructuredArrayIgnorePattern": "^_", + "caughtErrors": "all" + } ], - "settings": { - "import/extensions": [ - ".js", - ".jsx", - ".ts", - ".tsx" - ], - "import/parsers": { - "@typescript-eslint/parser": [ - ".ts", - ".tsx" - ] + // not-auto-fixable: No undefined variables allowed. + "no-undef": "error", + // not-auto-fixable: Dont use console statements. Use logger which babel will remove during bundling. + "no-console": "error", + // auto-fixable: sadly this doesn't support guard clauses yet. + "padding-line-between-statements": [ + "error", + { "blankLine": "always", "prev": "if", "next": ["if", "return"] }, + // The newline-before-return rule is deprecated in favor of the following: + { "blankLine": "always", "prev": "*", "next": "return" }, + // Add newline between function declarations + { + "blankLine": "always", + "prev": [ + "block", + "multiline-block-like", + "function", + "iife", + "multiline-const", + "multiline-expression", + ], + "next": ["function", "iife", "multiline-const", "multiline-expression"], }, - "import/resolver": { - "node": { - "extensions": [ - ".js", - ".jsx", - ".ts", - ".tsx" - ] - } + ], + // auto-fixable: Single line statements needn't have any braces. But in all other cases enforce curly braces. + "curly": ["error", "multi-line"], + // auto-fixable: Remove the else part, if the "if" or "else-if" chain has a return statement + "no-else-return": "error", + // not-auto-fixable: Prevent un-sanitized dangerouslySetInnerHTML. + "jam3/no-sanitizer-with-danger": [ + 2, + { + "wrapperName": ["dompurify", "sanitizer", "sanitize"], }, - "react": { - "pragma": "React", - "fragment": "Fragment", - "version": "detect" - } - }, - "plugins": [ - "react", - "import", - "@typescript-eslint" ], - "globals": { - "fetch": false, - "document": false, - "Promise": true, - "log": true, - "sessionStorage": true, - "localStorage": true, - "FileReader": true, - "window": true - }, - "rules": { - "@typescript-eslint/no-explicit-any": "off", - "object-curly-newline": "off", - "no-console": ["error", { "allow": ["warn", "error"] }], - "comma-dangle": ["error", "never"], - "indent": ["warn", 2, { "SwitchCase": 1 }], - "@typescript-eslint/no-unused-vars": ["error"], - "key-spacing": 1, - "keyword-spacing": 2, - "object-curly-spacing": [1, "always"], - "semi": 2, - "array-bracket-spacing": [2, "never"], - "arrow-body-style": ["error", "as-needed"], - "func-style": ["error", "expression"], - "space-before-function-paren": 2, - "no-multiple-empty-lines": ["warn", { "max": 1, "maxEOF": 1 }], - "quotes": ["warn", "double"], - "prefer-const": ["warn", { "destructuring": "any", "ignoreReadBeforeAssign": false }], - "no-var": 1, - "no-extra-boolean-cast": 1, - "no-unneeded-ternary": 1, - "react/no-unescaped-entities": 0, - "react/prop-types": 0, - "react/jsx-key": 0, - "import/order": ["error", { - "newlines-between": "always", - "alphabetize": { "order": "asc", "caseInsensitive": true }, - "warnOnUnassignedImports": true, - "groups": ["builtin", "external", "internal", "sibling", "parent", "index", "object", "type"], - "pathGroups": [ - { "pattern": "react", "group": "builtin", "position": "before" }, - { "pattern": "common/**", "group": "internal" }, - { "pattern": "context/**", "group": "internal" }, - { "pattern": "components/**", "group": "internal" }, - { "pattern": "assets/**", "group": "internal" }, - { "pattern": "apis/**", "group": "internal" }, - { "pattern": "constants/**", "group": "internal", "position": "after" }, - { "pattern": "utils/**", "group": "internal" }, - { "pattern": "helpers/**", "group": "internal" } - ], - "pathGroupsExcludedImportTypes": ["builtin"] - }] - } + // auto-fixable: Requires trailing commas when the last element or property is in a different line than the closing ] or } + "comma-dangle": [ + "error", + { + "arrays": "always-multiline", + "objects": "always-multiline", + "imports": "always-multiline", + "exports": "always-multiline", + "functions": "never", + }, + ], + // auto-fixable: If a variable is never reassigned, using the const declaration is better. + "prefer-const": "error", + // auto-fixable: It is considered good practice to use the type-safe equality operators === and !==. + // "eqeqeq": "error", + // not-auto-fixable: Rule flags optional chaining expressions in positions where short-circuiting to undefined causes throwing a TypeError afterward. + "no-unsafe-optional-chaining": "error", + // auto-fixable: Remove all unused imports. + "unused-imports/no-unused-imports": "error", + // auto-fixable-1-level-deep: Using nested ternary operators make the code unreadable. Use if/else or switch with if/else. If it's JSX then move it out into a function or a variable. It's fine to use nestedTernary in JSX when it makes code more readable. + "no-nested-ternary": "warn", + // auto-fixable: Enforces no braces where they can be omitted. + "arrow-body-style": ["error", "as-needed"], + // auto-fixable: Suggests using template literals instead of string concatenation. + "prefer-template": "error", + // auto-fixable: Disallows ternary operators when simpler alternatives exist. + "no-unneeded-ternary": ["error", { "defaultAssignment": false }], + // auto-fixable: Partially fixable. Prefer {x} over {x: x}. + "object-shorthand": [ + "error", + "always", + { "avoidQuotes": true, "ignoreConstructors": true }, + ], + // auto-fixable: Partially fixable. Unless there's a need to the this keyword, there's no advantage of using a plain function. + "prefer-arrow-callback": ["error", { "allowUnboundThis": true }], + // not-auto-fixable: Convert multiple imports from same module into a single import. + "no-duplicate-imports": ["error", { "includeExports": true }], + // auto-fixable: Partially fixable. In JavaScript, there are a lot of different ways to convert value types. Allow only readable coercions. + "no-implicit-coercion": ["error", { "allow": ["!!"] }], + // auto-fixable: Require let or const instead of var. + "no-var": "error", + // auto-fixable: This rule conflicts with prettier rules. Thus we've NOT kept this rule in react file. This rule ensures we don't add blank lines in JSX. + "react/jsx-newline": ["error", { "prevent": true }] } +} diff --git a/.husky/helpers/prevent_pushing_to_develop.sh b/.husky/helpers/prevent_pushing_to_develop.sh new file mode 100644 index 0000000000..6c33fde144 --- /dev/null +++ b/.husky/helpers/prevent_pushing_to_develop.sh @@ -0,0 +1,24 @@ +#!/bin/sh + +prevent_pushing_to_develop() { + current_branch=`git symbolic-ref HEAD` + current_origin=`git remote` + if [ current_origin = "origin" -o "$current_branch" = "refs/heads/develop" -o "$current_branch" = "refs/heads/main" ] + then + cat <= 0.72.0 + } + }, + "ruby.format": "rubocop", + } diff --git a/Gemfile b/Gemfile index e0af73f8cc..08ab4b7e91 100644 --- a/Gemfile +++ b/Gemfile @@ -145,14 +145,16 @@ group :development, :test do # Add Rubocop to lint and format Ruby code gem "rubocop", require: false - gem "rubocop-packaging", require: false gem "rubocop-performance", require: false gem "rubocop-rails", require: false - gem "rubocop-rspec", "~> 2.8", require: false + gem "rubocop-rspec", require: false # Use RSpec as the testing framework gem "rspec-rails", "~> 5.0", ">= 5.0.2" + # For linting ERB files + gem "erb_lint", require: false, git: "https://github.com/Shopify/erb-lint.git", branch: "main" + # Simple one-liner tests for common Rails functionality gem "shoulda-callback-matchers", "~> 1.1.1" gem "shoulda-matchers", "~> 5.1" diff --git a/Gemfile.lock b/Gemfile.lock index d1236bb2be..ae951ab60d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,3 +1,16 @@ +GIT + remote: https://github.com/Shopify/erb-lint.git + revision: 403c8a59f2998d361fbcde2350c06f01bea150d5 + branch: main + specs: + erb_lint (0.3.1) + activesupport + better_html (>= 2.0.1) + parser (>= 2.7.1.4) + rainbow + rubocop + smart_properties + GIT remote: https://github.com/heartcombo/devise revision: f8d1ea90bc328012f178b8a6616a89b73f2546a4 @@ -110,6 +123,13 @@ GEM babel-source (>= 4.0, < 6) execjs (~> 2.0) bcrypt (3.1.18) + better_html (2.0.1) + actionview (>= 6.0) + activesupport (>= 6.0) + ast (~> 2.0) + erubi (~> 1.4) + parser (>= 2.4) + smart_properties bindex (0.8.1) bootsnap (1.11.1) msgpack (~> 1.2) @@ -401,8 +421,6 @@ GEM unicode-display_width (>= 1.4.0, < 3.0) rubocop-ast (1.18.0) parser (>= 3.1.1.0) - rubocop-packaging (0.5.1) - rubocop (>= 0.89, < 2.0) rubocop-performance (1.14.0) rubocop (>= 1.7.0, < 2.0) rubocop-ast (>= 0.4.0) @@ -470,6 +488,7 @@ GEM simplecov-html (0.12.3) simplecov_json_formatter (0.1.4) sixarm_ruby_unaccent (1.2.0) + smart_properties (1.17.0) spring (4.0.0) sprockets (4.0.3) concurrent-ruby (~> 1.0) @@ -540,6 +559,7 @@ DEPENDENCIES discard (~> 1.2) dotenv-rails elasticsearch (< 7.14) + erb_lint! factory_bot_rails faker foreman @@ -566,10 +586,9 @@ DEPENDENCIES rolify (~> 6.0) rspec-rails (~> 5.0, >= 5.0.2) rubocop - rubocop-packaging rubocop-performance rubocop-rails - rubocop-rspec (~> 2.8) + rubocop-rspec ruby_audit sass-rails searchkick diff --git a/app/javascript/src/StyledComponents/Avatar.tsx b/app/javascript/src/StyledComponents/Avatar.tsx index 2c63a36f78..481b453ad7 100644 --- a/app/javascript/src/StyledComponents/Avatar.tsx +++ b/app/javascript/src/StyledComponents/Avatar.tsx @@ -17,17 +17,22 @@ const Avatar = ({ name = "", classNameImg = "", classNameInitials = "", - classNameInitialsWrapper = "" + classNameInitialsWrapper = "", }: AvatarProps) => { const [initials, setInitials] = useState(null); - const DEFAULT_STYLE_IMAGE = "inline-block md:h-10 md:w-10 h-5 w-5 rounded-full"; - const DEFAULT_STYLE_INITIALS = "md:text-xl text-xs md:font-medium font-light leading-none text-white"; - const DEFAULT_STYLE_INITIALS_WRAPPER = "inline-flex md:h-10 md:w-10 h-6 w-6 rounded-full items-center justify-center bg-gray-500"; + const DEFAULT_STYLE_IMAGE = + "inline-block md:h-10 md:w-10 h-5 w-5 rounded-full"; + + const DEFAULT_STYLE_INITIALS = + "md:text-xl text-xs md:font-medium font-light leading-none text-white"; + + const DEFAULT_STYLE_INITIALS_WRAPPER = + "inline-flex md:h-10 md:w-10 h-6 w-6 rounded-full items-center justify-center bg-gray-500"; const getInitials = () => { if (name) { const parts = name.match(/\b(\w)/g); - const initials = parts.join("").slice(0,2); + const initials = parts.join("").slice(0, 2); setInitials(initials.toUpperCase()); } }; @@ -37,12 +42,13 @@ const Avatar = ({ if (url) { return ( profile_pic ); } + if (initials) { return (
@@ -64,9 +70,9 @@ const Avatar = ({ return ( avatar ); }; diff --git a/app/javascript/src/StyledComponents/Badge.tsx b/app/javascript/src/StyledComponents/Badge.tsx index 1f49229ea1..5994e246d3 100644 --- a/app/javascript/src/StyledComponents/Badge.tsx +++ b/app/javascript/src/StyledComponents/Badge.tsx @@ -2,20 +2,21 @@ import React from "react"; import classnames from "classnames"; -const DEFAULT_STYLE = "inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-semibold leading-4 tracking-wider"; +const DEFAULT_STYLE = + "inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-semibold leading-4 tracking-wider"; type BadgeProps = { - text?: string | number, - color?: string, - bgColor?: string, - className?: string -} + text?: string | number; + color?: string; + bgColor?: string; + className?: string; +}; const Badge = ({ text = "Badge", color = "text-purple-800", bgColor = "bg-purple-100", - className + className, }: BadgeProps) => ( {text} diff --git a/app/javascript/src/StyledComponents/Button.tsx b/app/javascript/src/StyledComponents/Button.tsx index 3fc5b2b6c6..559f5a26c3 100644 --- a/app/javascript/src/StyledComponents/Button.tsx +++ b/app/javascript/src/StyledComponents/Button.tsx @@ -6,10 +6,13 @@ const DEFAULT_STYLE = "rounded text-center"; const PRIMARY = "bg-miru-han-purple-1000 hover:bg-miru-han-purple-600 text-white border border-miru-han-purple-1000 hover:border-miru-han-purple-600"; -const PRIMARY_DISABLED = "bg-miru-gray-1000 text-white border border-miru-gray-1000"; + +const PRIMARY_DISABLED = + "bg-miru-gray-1000 text-white border border-miru-gray-1000"; const SECONDARY = "bg-transparent hover:bg-miru-gray-1000 text-miru-han-purple-1000 border border-miru-han-purple-1000"; + const SECONDARY_DISABLED = "bg-transparent text-miru-dark-purple-200 border border-miru-dark-purple-200"; @@ -34,7 +37,7 @@ type ButtonProps = { const BUTTON_STYLES = { primary: "primary", secondary: "secondary", - ternary: "ternary" + ternary: "ternary", }; const SIZES = { small: "small", medium: "medium", large: "large" }; @@ -45,7 +48,7 @@ const Button = ({ className = "", fullWidth = false, onClick, - children + children, }: ButtonProps) => ( + />
); }; -const GetClientBar = ({ data, totalMinutes }:IChartBarGraph) => ( +const GetClientBar = ({ data, totalMinutes }: IChartBarGraph) => ( -

- TOTAL HOURS: {minToHHMM(totalMinutes)} +

+ TOTAL HOURS:{" "} + {minToHHMM(totalMinutes)}

-
- {data.map((element, index) => ) - } +
+ {data.map((element, index) => ( + + ))}
-
- - 0 - - - {minToHHMM(totalMinutes)} - +
+ 0 + {minToHHMM(totalMinutes)}
); diff --git a/app/javascript/src/common/ChartBar/interface.ts b/app/javascript/src/common/ChartBar/interface.ts index 0d979781f3..b2b456934a 100644 --- a/app/javascript/src/common/ChartBar/interface.ts +++ b/app/javascript/src/common/ChartBar/interface.ts @@ -3,7 +3,7 @@ interface ClientArray { } export interface IChartBar extends ClientArray { - handleSelectChange: any + handleSelectChange: any; totalMinutes: number; } diff --git a/app/javascript/src/common/CustomCheckbox.tsx b/app/javascript/src/common/CustomCheckbox.tsx index b3b09149cb..37f7c3f93b 100644 --- a/app/javascript/src/common/CustomCheckbox.tsx +++ b/app/javascript/src/common/CustomCheckbox.tsx @@ -1,4 +1,4 @@ -import * as React from "react"; +import React from "react"; import classnames from "classnames"; @@ -8,43 +8,49 @@ const CustomCheckbox = ({ checkboxValue, id, handleCheck, - name="", - wrapperClassName="", - labelClassName="" + name = "", + wrapperClassName = "", + labelClassName = "", }) => (
-
+
-
+
- +
- {text !== "" && ( -