first commit
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
import React, { memo, ReactNode, useCallback } from 'react';
|
||||
import { areEqual } from 'Utils/equalityChecks';
|
||||
import { Button } from 'Components/Button';
|
||||
|
||||
interface Props {
|
||||
children: ReactNode;
|
||||
item: any;
|
||||
disabled?: boolean;
|
||||
onClick: (e: any) => void;
|
||||
}
|
||||
|
||||
const ActionButtonWrapperContainer = ({ children, item, disabled, onClick }: Props) => {
|
||||
const onItemClick = useCallback(() => onClick(item), [item]);
|
||||
|
||||
return (
|
||||
<Button onClick={onItemClick} disabled={disabled}>
|
||||
{children}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
ActionButtonWrapperContainer.defaultProps = {
|
||||
disabled: false,
|
||||
};
|
||||
|
||||
const ActionButtonWrapperContainerMemo = memo(ActionButtonWrapperContainer, areEqual);
|
||||
|
||||
export { ActionButtonWrapperContainerMemo as ActionButtonWrapperContainer };
|
||||
@@ -0,0 +1 @@
|
||||
export { ActionButtonWrapperContainer as ActionButtonWrapper } from './ActionButtonWrapper';
|
||||
@@ -0,0 +1,104 @@
|
||||
import React, { memo, useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import { SingleUnitAdd } from 'Containers/SingleUnitAdd';
|
||||
import { MultiUnitAdd } from 'Containers/MultiUnitAdd';
|
||||
import { areEqual } from 'Utils/equalityChecks';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { setUnitType } from 'Containers/Projects';
|
||||
import { useHistory, useLocation } from 'react-router-dom';
|
||||
import { ADD_LOCATIONS, MULTI_UNIT, PHOTO_MANAGEMENT, SINGLE } from 'Utils/constants';
|
||||
import { MultiUnitAddRoom } from 'Containers/Project/AddLocations/MultiUnitAddRoom';
|
||||
import { setUnit } from 'Containers/Project/Unit/actions';
|
||||
import { multiUnitSelector, singleUnitSelector } from 'Containers/Project/Unit/selector';
|
||||
import { projectMultiSelector, propertyMultiSelector, propertySingleSelector } from 'Containers/Project/selectors';
|
||||
import { ChoosePropertyType } from './ChoosePropertyType';
|
||||
import { setSelectedUnitTypeUrl } from '.';
|
||||
|
||||
const AddLocationTabContainer = () => {
|
||||
const dispatch = useDispatch();
|
||||
const location = useLocation();
|
||||
const history = useHistory();
|
||||
|
||||
const { pathname } = location;
|
||||
|
||||
const { selectedProjectId, projectStore } = useSelector(projectMultiSelector, areEqual);
|
||||
const singleUnit: any = useSelector(singleUnitSelector, areEqual);
|
||||
const multiUnit: any = useSelector(multiUnitSelector, areEqual);
|
||||
const property: any = useSelector(propertySingleSelector, areEqual);
|
||||
const propertyMulti: any = useSelector(propertyMultiSelector, areEqual);
|
||||
|
||||
const [unitType] = useState({
|
||||
singleUnit: 1,
|
||||
multiUnit: 2,
|
||||
});
|
||||
const [singleUnitExists, setSingleUnitExists] = useState(false);
|
||||
const [multiUnitExists, setMultiUnitExists] = useState(false);
|
||||
|
||||
// TODO::replace these with redux
|
||||
useEffect(() => {
|
||||
const { id: singlUnitId } = singleUnit;
|
||||
const { id: multiUnitId } = multiUnit ?? '';
|
||||
const { id: propertyId } = property;
|
||||
const { id: propertyMultiId } = propertyMulti;
|
||||
|
||||
// single unit exist check
|
||||
const exists = Number.isSafeInteger(singlUnitId) && Number.isSafeInteger(propertyId);
|
||||
setSingleUnitExists(exists);
|
||||
|
||||
// Dispatch
|
||||
if (exists) {
|
||||
dispatch(setUnit(property, singleUnit, selectedProjectId));
|
||||
}
|
||||
|
||||
// multi unit exist check
|
||||
const existsMulti = Number.isSafeInteger(multiUnitId) && Number.isSafeInteger(propertyMultiId);
|
||||
setMultiUnitExists(existsMulti);
|
||||
}, [singleUnit, multiUnit, propertyMulti, property]);
|
||||
|
||||
const addSingleUnit = useCallback(() => {
|
||||
// Creat a unit here.
|
||||
|
||||
// Create the property adn Unit, based on the selected location type
|
||||
const projectAddress = projectStore.find((elem: any) => elem.id.toString() === selectedProjectId);
|
||||
|
||||
// we'll create single unit once, we'll skip if the user already has a single unit
|
||||
if (!singleUnitExists) {
|
||||
dispatch(setUnitType(selectedProjectId, projectAddress?.address?.address, unitType.singleUnit));
|
||||
}
|
||||
|
||||
history.push(`/projects${PHOTO_MANAGEMENT}${ADD_LOCATIONS}${SINGLE}`);
|
||||
dispatch(setSelectedUnitTypeUrl('singleUnit'));
|
||||
}, [projectStore, selectedProjectId, singleUnitExists]);
|
||||
|
||||
const addMultiUnit = useCallback(() => {
|
||||
history.push(`/projects${PHOTO_MANAGEMENT}${ADD_LOCATIONS}${MULTI_UNIT}`);
|
||||
dispatch(setSelectedUnitTypeUrl('multiUnit'));
|
||||
}, []);
|
||||
|
||||
const view = {
|
||||
mainView: <ChoosePropertyType onSingleUnitTileClick={addSingleUnit} onMultiUnitTileClick={addMultiUnit} />,
|
||||
singleUnitAddView: <SingleUnitAdd />,
|
||||
multiUnitAddView: <MultiUnitAdd />,
|
||||
multiUnitAddRoomView: <MultiUnitAddRoom />,
|
||||
};
|
||||
|
||||
const [tabView, setTabView] = useState(view.mainView);
|
||||
|
||||
useEffect(() => {
|
||||
if (pathname.includes(`/projects${PHOTO_MANAGEMENT}${ADD_LOCATIONS}${SINGLE}`)) {
|
||||
setTabView(view.singleUnitAddView);
|
||||
} else if (pathname.includes(`/projects${PHOTO_MANAGEMENT}${ADD_LOCATIONS}${MULTI_UNIT}/add`)) {
|
||||
setTabView(view.multiUnitAddRoomView);
|
||||
} else if (pathname.includes(`/projects${PHOTO_MANAGEMENT}${ADD_LOCATIONS}${MULTI_UNIT}`)) {
|
||||
setTabView(view.multiUnitAddView);
|
||||
} else if (!singleUnitExists && !multiUnitExists) {
|
||||
setTabView(view.mainView);
|
||||
}
|
||||
}, [pathname]);
|
||||
|
||||
return tabView;
|
||||
};
|
||||
|
||||
const AddLocationTabContainerMemo = memo(AddLocationTabContainer, areEqual);
|
||||
|
||||
export { AddLocationTabContainerMemo as AddLocationTab };
|
||||
@@ -0,0 +1,26 @@
|
||||
import React, { memo } from "react";
|
||||
|
||||
import { Icon } from "Components/Icons";
|
||||
import { ImageTile } from "Containers/ImageTile";
|
||||
import { areEqual } from "Utils/equalityChecks";
|
||||
|
||||
import classes from "./choosePropertyType.module.css";
|
||||
|
||||
interface Props {
|
||||
onSingleUnitTileClick: () => void;
|
||||
onMultiUnitTileClick: () => void;
|
||||
}
|
||||
|
||||
const ChoosePropertyType = ({ onSingleUnitTileClick, onMultiUnitTileClick }: Props) => (
|
||||
<div className={classes.addLocationsWrapper}>
|
||||
<h1 className={classes.addLocationsTitle}>This Property Is...</h1>
|
||||
<div className={`d-flex justify-content-center align-items-center ${classes.addLocationsContent}`}>
|
||||
<ImageTile caption="Single Unit" onTileClick={onSingleUnitTileClick} icon={<Icon type="singleHome" />} />
|
||||
<ImageTile caption="Multi Unit" onTileClick={onMultiUnitTileClick} icon={<Icon type="highrise" />} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const ChoosePropertyTypeMemo = memo(ChoosePropertyType, areEqual);
|
||||
|
||||
export { ChoosePropertyTypeMemo as ChoosePropertyType };
|
||||
@@ -0,0 +1,18 @@
|
||||
.addLocationsWrapper {
|
||||
margin-top: 16px;
|
||||
padding-bottom: 421px;
|
||||
}
|
||||
|
||||
.addLocationsTitle {
|
||||
font-family: IBM Plex Sans;
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-size: 32px;
|
||||
line-height: 40px;
|
||||
color: #0e1c4a;
|
||||
margin-left: 30px;
|
||||
}
|
||||
|
||||
.addLocationsContent {
|
||||
margin-top: 115px;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { ChoosePropertyType } from "./ChoosePropertyType";
|
||||
@@ -0,0 +1,19 @@
|
||||
// types
|
||||
export const SET_UNIT_TYPE_URL = "SET_UNIT_TYPE_URL";
|
||||
|
||||
interface ActionTypes {
|
||||
SET_UNIT_TYPE_URL: string;
|
||||
}
|
||||
|
||||
interface MessageAction {
|
||||
type: keyof ActionTypes;
|
||||
payload: string;
|
||||
}
|
||||
|
||||
// TODO::ADD THUNKS FOR DIFFERENT TYPES OF SPINNERS
|
||||
|
||||
export const setSelectedUnitTypeUrl = (payload: string) => ({
|
||||
type: SET_UNIT_TYPE_URL,
|
||||
payload,
|
||||
});
|
||||
export type SetUnitTypes = MessageAction;
|
||||
@@ -0,0 +1,3 @@
|
||||
export { AddLocationTab } from "./AddLocationTab";
|
||||
export { addLocationsReducer } from "./reducer";
|
||||
export { setSelectedUnitTypeUrl } from "./actions";
|
||||
@@ -0,0 +1,37 @@
|
||||
// types
|
||||
import { SET_UNIT_TYPE_URL, SetUnitTypes } from './actions';
|
||||
|
||||
const routeBasePath: string = '/projects/photoManagement/';
|
||||
const singleUnitTypeRoute: string = 'addLocations/single';
|
||||
const multiUnitTypeRoute: string = 'addLocations/multiUnit/add';
|
||||
const allLocationsRoute: string = 'allLocations';
|
||||
let selectedRoute = `${routeBasePath}${allLocationsRoute}`;
|
||||
|
||||
// state
|
||||
const initialState = {
|
||||
selectedUnitTypeUrl: selectedRoute,
|
||||
};
|
||||
|
||||
// we we'll use this for system variables, etc. loaders
|
||||
export const addLocationsReducer = (state = initialState, action: SetUnitTypes) => {
|
||||
switch (action.type) {
|
||||
case SET_UNIT_TYPE_URL: {
|
||||
const selectedPath: string = action.payload;
|
||||
|
||||
if (selectedPath === singleUnitTypeRoute) {
|
||||
selectedRoute = `${routeBasePath}${singleUnitTypeRoute}`;
|
||||
}
|
||||
|
||||
if (selectedPath === multiUnitTypeRoute) {
|
||||
selectedRoute = `${routeBasePath}${multiUnitTypeRoute}`;
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
selectedUnitTypeUrl: selectedRoute,
|
||||
};
|
||||
}
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export const selectedUnitTypeUrlSelector = ({ addLocations: { selectedUnitTypeUrl } }) => selectedUnitTypeUrl;
|
||||
@@ -0,0 +1,154 @@
|
||||
import React, { memo, useCallback, useEffect, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { areEqual } from 'Utils/equalityChecks';
|
||||
|
||||
import { listCountries, setProvincesList } from 'Containers/Address/actions';
|
||||
import { CountryModel } from 'Containers/Address/CountryAutocomplete/Models/CountryModel';
|
||||
|
||||
import { CountryAutocomplete } from 'Components/Address';
|
||||
|
||||
// set country value if passed from the parent (redux variable)
|
||||
const getCountryLongName = (countriesList: Array<CountryModel>, value: string) =>
|
||||
countriesList.find((country: CountryModel) => country.alpha_2.toLocaleLowerCase() === value.toLocaleLowerCase());
|
||||
|
||||
const countriesSelector = ({ address: { countries } }: any) => countries;
|
||||
|
||||
interface Props {
|
||||
country: string;
|
||||
setSelectedCountry: (e: any) => void;
|
||||
invalid?: boolean;
|
||||
showCaretIcon?: boolean;
|
||||
}
|
||||
|
||||
const CountryAutocompleteContainer = ({ country, setSelectedCountry, invalid, showCaretIcon }: Props) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const [countryLocal, setCountryLocal] = useState('');
|
||||
const [countries, setCountries] = useState([]);
|
||||
const [showDropDown, setShowDropDown] = useState(false);
|
||||
|
||||
// API data
|
||||
const countriesList: Array<CountryModel> = useSelector(countriesSelector, areEqual);
|
||||
useEffect(() => {
|
||||
if (countriesList.length === 0) {
|
||||
dispatch(listCountries());
|
||||
}
|
||||
}, []);
|
||||
|
||||
// we'll use this to prevent the if condition being running onSelectItem useCallback
|
||||
const [clear, setClear] = useState(true);
|
||||
useEffect(() => {
|
||||
// if the country field sent, we'll set the local country state and provinces
|
||||
// this happen when a user choose an address from google
|
||||
if (country && countriesList.length > 0 && clear) {
|
||||
let provinces = [];
|
||||
const lowercase = country.toLocaleLowerCase();
|
||||
|
||||
// if the country selected from google autocomplete we do the following
|
||||
if (country.length <= 3) {
|
||||
setCountryLocal(getCountryLongName(countriesList, country)?.name);
|
||||
|
||||
const { provinces: provinceList } = countriesList.find(
|
||||
(country: CountryModel) => country.alpha_2.toLocaleLowerCase() === lowercase
|
||||
);
|
||||
|
||||
provinces = provinceList;
|
||||
} else {
|
||||
setCountryLocal(country);
|
||||
|
||||
provinces = countriesList.find(
|
||||
(country: CountryModel) => country.name.toLocaleLowerCase() === lowercase
|
||||
)?.provinces;
|
||||
}
|
||||
|
||||
if (provinces) {
|
||||
dispatch(setProvincesList(provinces));
|
||||
}
|
||||
}
|
||||
}, [country, countriesList]);
|
||||
|
||||
useEffect(() => {
|
||||
if (countriesList.length > 0) {
|
||||
setCountries(countriesList);
|
||||
}
|
||||
}, [countriesList]);
|
||||
|
||||
const onChangeCountry = useCallback(
|
||||
(e: any) => {
|
||||
e.preventDefault();
|
||||
|
||||
setClear(false);
|
||||
const { value } = e.target;
|
||||
|
||||
if (value.length > 0) {
|
||||
const lowercase = value.toLocaleLowerCase();
|
||||
const countries = countriesList.filter((country: CountryModel) =>
|
||||
country.name.toLocaleLowerCase().includes(lowercase)
|
||||
);
|
||||
setCountries(countries);
|
||||
setShowDropDown(countries.length > 0);
|
||||
} else {
|
||||
dispatch(setProvincesList([]));
|
||||
}
|
||||
setSelectedCountry(value);
|
||||
setCountryLocal(value);
|
||||
},
|
||||
[countriesList]
|
||||
);
|
||||
|
||||
const onSelectItem = useCallback(
|
||||
(e: any) => {
|
||||
e.preventDefault();
|
||||
|
||||
const { id = '', name = '' } = e.target.dataset;
|
||||
|
||||
setCountryLocal(name);
|
||||
setSelectedCountry(name);
|
||||
|
||||
const { provinces } = countriesList.find((country: CountryModel) => country.id.toString() === id);
|
||||
|
||||
if (provinces) {
|
||||
dispatch(setProvincesList(provinces));
|
||||
}
|
||||
|
||||
setShowDropDown(false);
|
||||
|
||||
// set this to false, so the if condition in the first useEffect will fail
|
||||
setClear(false);
|
||||
},
|
||||
[countriesList]
|
||||
);
|
||||
|
||||
const onClickCaretIcon = useCallback(() => {
|
||||
setShowDropDown((prevState) => !prevState);
|
||||
}, []);
|
||||
|
||||
// we'll close the dropdown onBlur event outside the dropdown
|
||||
// const onBlur = useCallback((e: any) => {
|
||||
// e.preventDefault();
|
||||
// setShowDropDown(false);
|
||||
// }, []);
|
||||
|
||||
return (
|
||||
<CountryAutocomplete
|
||||
country={countryLocal}
|
||||
onChangeCountry={onChangeCountry}
|
||||
countries={countries}
|
||||
onSelectItem={onSelectItem}
|
||||
onClickCaretIcon={onClickCaretIcon}
|
||||
showCaretIcon={showCaretIcon}
|
||||
showDropDown={showDropDown}
|
||||
invalid={invalid}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
CountryAutocompleteContainer.defaultProps = {
|
||||
invalid: false,
|
||||
showCaretIcon: false,
|
||||
};
|
||||
|
||||
const CountryAutocompleteContainerMemo = memo(CountryAutocompleteContainer, areEqual);
|
||||
|
||||
export { CountryAutocompleteContainerMemo as CountryAutocompleteContainer };
|
||||
@@ -0,0 +1,11 @@
|
||||
/* eslint-disable */
|
||||
|
||||
import { ProvinceModel } from 'Containers/Address/ProvinceAutocomplete/Models/ProvinceModel';
|
||||
|
||||
export type CountryModel = {
|
||||
id: number;
|
||||
name: string;
|
||||
alpha_2: string;
|
||||
alpha_3: string;
|
||||
provinces: Array<ProvinceModel>;
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export type { CountryModel } from './CountryModel';
|
||||
@@ -0,0 +1 @@
|
||||
export { CountryAutocompleteContainer as CountryAutocomplete } from './CountryAutocomplete';
|
||||
@@ -0,0 +1,172 @@
|
||||
import React, { memo, useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
|
||||
import usePlacesService from 'react-google-autocomplete/lib/usePlacesAutocompleteService';
|
||||
import { areEqual } from 'Utils/equalityChecks';
|
||||
|
||||
import { setAddressRequest } from 'Containers/Address/actions';
|
||||
|
||||
import { GoogleAutocomplete } from 'Components/Address';
|
||||
|
||||
import { componentRestrictions } from 'Utils/google';
|
||||
|
||||
const googleAutoCompleteConfig = {
|
||||
debounce: 700,
|
||||
};
|
||||
|
||||
const GoogleAutocompleteContainer = () => {
|
||||
const dispatch = useDispatch();
|
||||
const history = useHistory();
|
||||
|
||||
const { placesService, placePredictions, getPlacePredictions } = usePlacesService(googleAutoCompleteConfig);
|
||||
|
||||
// local variables
|
||||
const [address, setAddress]: any = useState('');
|
||||
const [showDropDown, setShowDropDown]: any = useState(false);
|
||||
const [placeDetails, setPlacesDetails]: any = useState('');
|
||||
const [localPlacePredictions, setLocalPlacePredictions]: any = useState([]);
|
||||
const [placeId, setPlaceId]: any = useState('');
|
||||
|
||||
// we'll use these objects to get accurate data from the google
|
||||
const [autocompleteAddress, setAutocompleteAddress] = useState({
|
||||
street_number: '',
|
||||
route: '',
|
||||
locality: '',
|
||||
administrative_area_level_1: '',
|
||||
country: '',
|
||||
postal_code: '',
|
||||
});
|
||||
const [addressComponents] = useState({
|
||||
street_number: 'short_name',
|
||||
route: 'long_name',
|
||||
locality: 'long_name',
|
||||
administrative_area_level_1: 'long_name',
|
||||
country: 'short_name',
|
||||
postal_code: 'short_name',
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
// prepare the local data object, add or modify any property as needed
|
||||
if (placePredictions.length) {
|
||||
setLocalPlacePredictions(
|
||||
placePredictions.map((place: any) => ({
|
||||
placeId: place.place_id,
|
||||
mainText: place?.structured_formatting?.main_text,
|
||||
secondaryText: place?.structured_formatting?.secondary_text,
|
||||
}))
|
||||
);
|
||||
}
|
||||
}, [placePredictions]);
|
||||
|
||||
useEffect(() => {
|
||||
// show the dropdown once the data is prepared
|
||||
setShowDropDown(localPlacePredictions.length > 0);
|
||||
}, [localPlacePredictions]);
|
||||
|
||||
// on set placeId, we'll call google API to get place details
|
||||
useEffect(() => {
|
||||
if (placeId) {
|
||||
placesService?.getDetails({ placeId }, (placeDetails) => {
|
||||
setPlacesDetails(placeDetails);
|
||||
});
|
||||
}
|
||||
}, [placeId]);
|
||||
|
||||
// this is where we set our local variables, redux variables
|
||||
useEffect(() => {
|
||||
if (placeDetails) {
|
||||
setAddress(placeDetails?.formatted_address);
|
||||
|
||||
const address: any = {};
|
||||
const components: any = placeDetails?.address_components || [];
|
||||
|
||||
components.forEach((component: any) => {
|
||||
const [addressType = ''] = component?.types;
|
||||
|
||||
if (addressType.length > 0) {
|
||||
if (addressComponents[addressType]) {
|
||||
// set address objects with google data
|
||||
address[addressType] = addressType.length > 0 ? component[addressComponents[addressType]] : '';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// update state
|
||||
setAutocompleteAddress({
|
||||
...autocompleteAddress,
|
||||
...address,
|
||||
});
|
||||
}
|
||||
}, [placeDetails]);
|
||||
|
||||
// here we'll update the redux and trigger the route change
|
||||
useEffect(() => {
|
||||
if (autocompleteAddress.country) {
|
||||
dispatch(
|
||||
setAddressRequest({
|
||||
google_places_id: placeId,
|
||||
country: autocompleteAddress.country,
|
||||
state: autocompleteAddress.administrative_area_level_1,
|
||||
city: autocompleteAddress.locality,
|
||||
zip: autocompleteAddress.postal_code,
|
||||
address: `${autocompleteAddress.street_number} ${autocompleteAddress.route}`,
|
||||
latitude: placeDetails?.geometry?.location?.lat(),
|
||||
longitude: placeDetails?.geometry?.location?.lng(),
|
||||
})
|
||||
);
|
||||
|
||||
setShowDropDown(false);
|
||||
history.push('/projects/editAddress');
|
||||
}
|
||||
}, [autocompleteAddress]);
|
||||
|
||||
const onChangeAddress = useCallback((e: any) => {
|
||||
const { value } = e.target;
|
||||
|
||||
// this will call the google API and get the predictions
|
||||
getPlacePredictions({ input: value, componentRestrictions });
|
||||
|
||||
// to set local input value
|
||||
setAddress(value);
|
||||
}, []);
|
||||
|
||||
const onSelectItem = useCallback(
|
||||
(e: any) => {
|
||||
// set placeId
|
||||
const tempPlaceId = e.target.dataset?.id;
|
||||
if (tempPlaceId) {
|
||||
setPlaceId(tempPlaceId);
|
||||
} else {
|
||||
// sometimes id not present in the target element, so we'll get the id from the parent(Anchor) tag
|
||||
// this happen when they click on the Span tag
|
||||
setPlaceId(e.target.parentElement.dataset.id);
|
||||
}
|
||||
|
||||
// close the dropdown
|
||||
setShowDropDown(false);
|
||||
},
|
||||
[localPlacePredictions]
|
||||
);
|
||||
|
||||
// close the dropdown when click outside the dropdown
|
||||
const onblur = useCallback(() => {
|
||||
setShowDropDown(false);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<GoogleAutocomplete
|
||||
address={address}
|
||||
onChangeAddress={onChangeAddress}
|
||||
placePredictions={localPlacePredictions}
|
||||
onSelectItem={onSelectItem}
|
||||
showDropDown={showDropDown}
|
||||
onblur={onblur}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const GoogleAutocompleteContainerMemo = memo(GoogleAutocompleteContainer, areEqual);
|
||||
|
||||
export { GoogleAutocompleteContainerMemo as GoogleAutocompleteContainer };
|
||||
@@ -0,0 +1 @@
|
||||
export { GoogleAutocompleteContainer as GoogleAutocomplete } from "./GoogleAutocomplete";
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
/* eslint-disable */
|
||||
|
||||
export type ProvinceModel = {
|
||||
id: number;
|
||||
name: string;
|
||||
alpha_2: string;
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export type { ProvinceModel } from './ProvinceModel';
|
||||
@@ -0,0 +1,99 @@
|
||||
import React, { memo, useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import { areEqual } from 'Utils/equalityChecks';
|
||||
|
||||
import { ProvinceAutocomplete } from 'Components/Address';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
interface Props {
|
||||
state: string;
|
||||
setSelectedProvince: (e: any) => void;
|
||||
invalid?: boolean;
|
||||
showCaretIcon?: boolean;
|
||||
}
|
||||
|
||||
const provincesSelector = ({ address: { provinces } }: any) => provinces;
|
||||
|
||||
const ProvinceAutocompleteContainer = ({ state, setSelectedProvince, invalid, showCaretIcon }: Props) => {
|
||||
const [province, setProvince] = useState('');
|
||||
const [provinces, setProvinces] = useState([]);
|
||||
const [showDropDown, setShowDropDown] = useState(false);
|
||||
|
||||
// API data
|
||||
const provincesList = useSelector(provincesSelector, areEqual);
|
||||
|
||||
// set province value if passed from the parent (redux variable)
|
||||
useEffect(() => {
|
||||
setProvince(state);
|
||||
}, [state]);
|
||||
|
||||
useEffect(() => {
|
||||
if (provincesList.length > 0) {
|
||||
setProvinces(provincesList);
|
||||
}
|
||||
}, [provincesList]);
|
||||
|
||||
const onChangeProvince = useCallback(
|
||||
(e: any) => {
|
||||
e.preventDefault();
|
||||
|
||||
const { value } = e.target;
|
||||
const lowercase = value.toLocaleLowerCase();
|
||||
setProvinces(
|
||||
value.length > 0
|
||||
? provincesList.filter((province: any) => province.name.toLocaleLowerCase().includes(lowercase))
|
||||
: []
|
||||
);
|
||||
setProvince(value);
|
||||
setSelectedProvince(value);
|
||||
setShowDropDown(value.length > 0);
|
||||
},
|
||||
[provincesList]
|
||||
);
|
||||
|
||||
const onSelectItem = useCallback((e: any) => {
|
||||
e.preventDefault();
|
||||
|
||||
const { name = '' } = e.target.dataset;
|
||||
|
||||
setProvince(name);
|
||||
setSelectedProvince(name);
|
||||
|
||||
setShowDropDown(false);
|
||||
}, []);
|
||||
|
||||
// useEffect(() => {
|
||||
// setShowDropDown(province.length > 0 && provinces.length > 0);
|
||||
// }, [provinces]);
|
||||
|
||||
const onClickCaretIcon = useCallback(() => {
|
||||
setShowDropDown((prevState) => !prevState);
|
||||
}, []);
|
||||
|
||||
// const onBlur = useCallback((e: any) => {
|
||||
// e.preventDefault();
|
||||
// setShowDropDown(false);
|
||||
// }, []);
|
||||
|
||||
return (
|
||||
<ProvinceAutocomplete
|
||||
province={province}
|
||||
onChangeProvince={onChangeProvince}
|
||||
provinces={provinces}
|
||||
onSelectItem={onSelectItem}
|
||||
showDropDown={showDropDown}
|
||||
invalid={invalid}
|
||||
showCaretIcon={showCaretIcon}
|
||||
onClickCaretIcon={onClickCaretIcon}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
ProvinceAutocompleteContainer.defaultProps = {
|
||||
invalid: false,
|
||||
showCaretIcon: false,
|
||||
};
|
||||
|
||||
const ProvinceAutocompleteContainerMemo = memo(ProvinceAutocompleteContainer, areEqual);
|
||||
|
||||
export { ProvinceAutocompleteContainerMemo as ProvinceAutocompleteContainer };
|
||||
@@ -0,0 +1 @@
|
||||
export { ProvinceAutocompleteContainer as ProvinceAutocomplete } from './ProvinceAutocomplete';
|
||||
@@ -0,0 +1,112 @@
|
||||
/* eslint-disable */
|
||||
|
||||
import { handleApiRequest } from 'Utils/handleApiRequest';
|
||||
|
||||
// types
|
||||
import { CountryModel } from 'Containers/Address/CountryAutocomplete/Models/CountryModel';
|
||||
import { ProvinceModel } from 'Containers/Address/ProvinceAutocomplete/Models/ProvinceModel';
|
||||
import { userDetails } from 'Containers/User';
|
||||
import { projectsCreate } from 'Containers/Projects/actions';
|
||||
import { setProjectAddressObject, setSimpleProjectAddress } from 'Containers/RocketScan/actions';
|
||||
|
||||
export const SET_COUNTRIES = 'SET_COUNTRIES';
|
||||
export const SET_PROVINCES = 'SET_PROVINCES';
|
||||
export const SET_ADDRESS_REQUEST = 'SET_ADDRESS_REQUEST';
|
||||
export const RESET_ADDRESS_REQUEST = 'RESET_ADDRESS_REQUEST';
|
||||
|
||||
interface ActionTypes {
|
||||
SET_COUNTRIES: Array<CountryModel> | null;
|
||||
SET_PROVINCES: Array<ProvinceModel> | null;
|
||||
SET_ADDRESS_REQUEST: any;
|
||||
RESET_ADDRESS_REQUEST: any;
|
||||
}
|
||||
|
||||
interface MessageAction {
|
||||
type: keyof ActionTypes;
|
||||
payload: any;
|
||||
}
|
||||
|
||||
export type SetAddressTypes = MessageAction;
|
||||
|
||||
export const listCountries =
|
||||
(url = 'countries', type = 'get', requestData = {}) =>
|
||||
async (dispatch: any, _getState = null, utils: any) => {
|
||||
const response = await handleApiRequest(dispatch, utils.Api[type](url, requestData));
|
||||
|
||||
if (response?.data) {
|
||||
const { data } = response;
|
||||
|
||||
dispatch({
|
||||
type: SET_COUNTRIES,
|
||||
payload: data,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// create address for project
|
||||
export const addressCreate =
|
||||
(requestData = {}, companyId: number, projectStatusId = '', url = 'addresses', type = 'post') =>
|
||||
async (dispatch: any, _getState = null, utils: any) => {
|
||||
const response = await handleApiRequest(dispatch, utils.Api[type](url, requestData));
|
||||
|
||||
if (response?.data) {
|
||||
const { data } = response;
|
||||
|
||||
dispatch(
|
||||
projectsCreate(
|
||||
{
|
||||
company_id: companyId,
|
||||
project_status_id: projectStatusId,
|
||||
address_id: data.id,
|
||||
},
|
||||
`companies/${companyId}/projects`
|
||||
)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// update address
|
||||
export const addressUpdate =
|
||||
(addressId: number, requestData = {}, onAddressUpdated?: any) =>
|
||||
async (dispatch: any, _getState = null, utils: any) => {
|
||||
const response = await handleApiRequest(dispatch, utils.Api.put(`addresses/${addressId}`, requestData));
|
||||
|
||||
if (response?.data) {
|
||||
const { data } = response;
|
||||
|
||||
dispatch(setProjectAddressObject(data));
|
||||
dispatch(setSimpleProjectAddress(data.address));
|
||||
}
|
||||
|
||||
if (onAddressUpdated) {
|
||||
onAddressUpdated();
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
* Non API thunks
|
||||
* */
|
||||
|
||||
export const setProvincesList =
|
||||
(data = []) =>
|
||||
async (dispatch) => {
|
||||
dispatch({
|
||||
type: SET_PROVINCES,
|
||||
payload: data,
|
||||
});
|
||||
};
|
||||
|
||||
export const setAddressRequest =
|
||||
(data = {}) =>
|
||||
async (dispatch) => {
|
||||
dispatch({
|
||||
type: SET_ADDRESS_REQUEST,
|
||||
payload: data,
|
||||
});
|
||||
};
|
||||
|
||||
export const resetAddressRequest = () => async (dispatch) => {
|
||||
dispatch({
|
||||
type: RESET_ADDRESS_REQUEST,
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
export { CountryAutocomplete } from "./CountryAutocomplete";
|
||||
export { ProvinceAutocomplete } from "./ProvinceAutocomplete";
|
||||
export { GoogleAutocomplete } from "./GoogleAutocomplete";
|
||||
@@ -0,0 +1,51 @@
|
||||
// state
|
||||
import {
|
||||
SET_ADDRESS_REQUEST,
|
||||
RESET_ADDRESS_REQUEST,
|
||||
SET_COUNTRIES,
|
||||
SET_PROVINCES,
|
||||
SetAddressTypes,
|
||||
} from "Containers/Address/actions";
|
||||
|
||||
const initialState = {
|
||||
countries: [],
|
||||
provinces: [],
|
||||
addressRequest: {
|
||||
google_places_id: "",
|
||||
address: "",
|
||||
addressTwo: "",
|
||||
city: "",
|
||||
state: "",
|
||||
zip: "",
|
||||
country: "",
|
||||
latitude: "",
|
||||
longitude: "",
|
||||
},
|
||||
};
|
||||
|
||||
export const addressReducer = (state = initialState, action: SetAddressTypes) => {
|
||||
switch (action.type) {
|
||||
case SET_COUNTRIES:
|
||||
return {
|
||||
...state,
|
||||
countries: action.payload,
|
||||
};
|
||||
case SET_PROVINCES:
|
||||
return {
|
||||
...state,
|
||||
provinces: action.payload,
|
||||
};
|
||||
case SET_ADDRESS_REQUEST:
|
||||
return {
|
||||
...state,
|
||||
addressRequest: { ...state.addressRequest, ...action.payload },
|
||||
};
|
||||
case RESET_ADDRESS_REQUEST:
|
||||
return {
|
||||
...state,
|
||||
addressRequest: initialState.addressRequest,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
import React, { memo, ReactNode } from 'react';
|
||||
|
||||
import { areEqual } from 'Utils/equalityChecks';
|
||||
|
||||
import { GuestLayout } from 'Components/Layouts';
|
||||
|
||||
interface Props {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
const GuestWrapperContainer = ({ children }: Props) => <GuestLayout>{children}</GuestLayout>;
|
||||
|
||||
const GuestWrapperContainerMemo = memo(GuestWrapperContainer, areEqual);
|
||||
|
||||
export { GuestWrapperContainerMemo as GuestWrapperContainer };
|
||||
@@ -0,0 +1 @@
|
||||
export { GuestWrapperContainer as GuestWrapper } from "./GuestWrapper";
|
||||
@@ -0,0 +1,145 @@
|
||||
/* eslint-disable */
|
||||
|
||||
// types
|
||||
import { SMS_SENT } from 'Containers/SignIn/PhoneVerification/actions';
|
||||
|
||||
export const SET_AUTH_TOKEN = 'SET_AUTH_TOKEN';
|
||||
export const SET_AUTHENTICATED = 'SET_AUTHENTICATED';
|
||||
export const SET_AUTHENTICATION_TYPE = 'SET_AUTHENTICATION_TYPE';
|
||||
export const SOCIAL_LOGIN_ERRORS = 'SOCIAL_LOGIN_ERRORS';
|
||||
export const SET_RESET_AUTH = 'SET_RESET_AUTH';
|
||||
|
||||
interface ActionTypes {
|
||||
SET_AUTH_TOKEN: string;
|
||||
SET_AUTHENTICATED: boolean;
|
||||
SET_AUTHENTICATION_TYPE: string;
|
||||
SOCIAL_LOGIN_ERRORS: object;
|
||||
SET_RESET_AUTH: string;
|
||||
}
|
||||
|
||||
interface MessageAction {
|
||||
type: keyof ActionTypes;
|
||||
payload: any;
|
||||
}
|
||||
|
||||
export type SetAuthTypes = MessageAction;
|
||||
|
||||
import { userDetails } from 'Containers/User';
|
||||
// API
|
||||
import { handleApiRequest } from 'Utils/handleApiRequest';
|
||||
import { CODE_VERIFIED } from 'Containers/SignIn/PhoneVerificationCode';
|
||||
import { createPhoneRecord, SET_USER_STATUS, SET_USER_VERIFICATION } from 'Containers/User/actions';
|
||||
|
||||
export const login =
|
||||
(url: string, type = 'post', requestData = {}) =>
|
||||
async (dispatch: any, _getState = null, utils: any) => {
|
||||
const data = await handleApiRequest(dispatch, utils.Api[type](url, requestData));
|
||||
if (data) {
|
||||
const { token } = data;
|
||||
// update the axios headers
|
||||
utils.Api.setAuthorizationToken(token);
|
||||
|
||||
// we'll fetch user details on a successful sign in and set redux variable for route changes
|
||||
dispatch(userDetails());
|
||||
|
||||
// set the redux state with the token --sanctum auth bearer token
|
||||
dispatch({
|
||||
type: SET_AUTH_TOKEN,
|
||||
payload: token,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const register =
|
||||
(url = 'auth/register', type = 'post', requestData = {}) =>
|
||||
async (dispatch: any, _getState = null, utils: any) => {
|
||||
const data = await handleApiRequest(dispatch, utils.Api[type](url, requestData));
|
||||
if (data) {
|
||||
const { token } = data;
|
||||
// update the axios headers
|
||||
utils.Api.setAuthorizationToken(token);
|
||||
|
||||
dispatch(userDetails());
|
||||
|
||||
// set the redux state with the token --sanctum auth bearer token
|
||||
dispatch({
|
||||
type: SET_AUTH_TOKEN,
|
||||
payload: token,
|
||||
});
|
||||
|
||||
// set the redux state with the authentication status
|
||||
dispatch({
|
||||
type: SET_AUTHENTICATED,
|
||||
payload: true,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const smsSendVerification =
|
||||
(url = 'auth/sms-send-verification', type = 'post', requestData: any, userID: number) =>
|
||||
async (dispatch: any, _getState = null, utils: any) => {
|
||||
const data = await handleApiRequest(dispatch, utils.Api[type](url, requestData));
|
||||
|
||||
if (typeof data === 'string') {
|
||||
const { phone } = requestData;
|
||||
|
||||
// create a phone record so we can attach it to the user
|
||||
dispatch(
|
||||
createPhoneRecord(
|
||||
'phones',
|
||||
'post',
|
||||
{
|
||||
value: phone,
|
||||
extension: '',
|
||||
is_primary: true,
|
||||
type: 'mobile',
|
||||
},
|
||||
userID
|
||||
)
|
||||
);
|
||||
|
||||
dispatch({
|
||||
type: SMS_SENT,
|
||||
payload: true,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const smsVerifyCode =
|
||||
(url = 'auth/sms-verify-code', type = 'post', requestData = {}) =>
|
||||
async (dispatch: any, _getState = null, utils: any) => {
|
||||
const data = await handleApiRequest(dispatch, utils.Api[type](url, requestData));
|
||||
|
||||
if (typeof data === 'string') {
|
||||
dispatch({
|
||||
type: CODE_VERIFIED,
|
||||
payload: true,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const logout =
|
||||
(url = 'auth/logout') =>
|
||||
async (dispatch, _getState = null, utils) => {
|
||||
// clear the axios headers
|
||||
utils.Api.resetAuthorizationToken();
|
||||
|
||||
await utils.Api.delete(url, {});
|
||||
|
||||
// clear auth data
|
||||
dispatch({
|
||||
type: SET_RESET_AUTH,
|
||||
});
|
||||
|
||||
// clear user data, important for route changes
|
||||
dispatch({
|
||||
type: SET_USER_VERIFICATION,
|
||||
payload: {
|
||||
authenticated: false,
|
||||
sms: false,
|
||||
company: false,
|
||||
approved: false,
|
||||
isNew: false,
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
export { GuestWrapper } from "./GuestWrapper";
|
||||
export { login, logout } from "./actions";
|
||||
export { AuthReducer } from "./reducer";
|
||||
@@ -0,0 +1,52 @@
|
||||
// types
|
||||
import {
|
||||
SET_AUTH_TOKEN,
|
||||
SET_AUTHENTICATED,
|
||||
SET_AUTHENTICATION_TYPE,
|
||||
SET_RESET_AUTH,
|
||||
SetAuthTypes,
|
||||
SOCIAL_LOGIN_ERRORS,
|
||||
} from './actions';
|
||||
|
||||
// state
|
||||
const initialState = {
|
||||
token: '',
|
||||
authenticated: false,
|
||||
authenticationType: 'basic',
|
||||
socialLoginErrors: {
|
||||
errors: false,
|
||||
message: '',
|
||||
},
|
||||
};
|
||||
|
||||
export const AuthReducer = (state = initialState, action: SetAuthTypes) => {
|
||||
switch (action.type) {
|
||||
case SET_AUTH_TOKEN:
|
||||
return {
|
||||
...state,
|
||||
token: action.payload,
|
||||
};
|
||||
case SET_AUTHENTICATED:
|
||||
return {
|
||||
...state,
|
||||
authenticated: action.payload,
|
||||
};
|
||||
case SET_AUTHENTICATION_TYPE:
|
||||
return {
|
||||
...state,
|
||||
authenticationType: action.payload,
|
||||
};
|
||||
case SOCIAL_LOGIN_ERRORS:
|
||||
return {
|
||||
...state,
|
||||
socialLoginErrors: {
|
||||
...state.socialLoginErrors,
|
||||
...action.payload,
|
||||
},
|
||||
};
|
||||
case SET_RESET_AUTH:
|
||||
return initialState;
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export const authenticatedSelector = ({ auth: { authenticated: value = false } }: any) => value;
|
||||
@@ -0,0 +1,58 @@
|
||||
import React, { memo, useEffect, useRef } from 'react';
|
||||
import { areEqual } from 'Utils/equalityChecks';
|
||||
import { TextBox } from 'Components/TextBox';
|
||||
|
||||
import formClasses from 'Themes/form/form.module.css';
|
||||
|
||||
interface Props {
|
||||
name: string;
|
||||
ariaLabel: string;
|
||||
placeholder: string;
|
||||
value: string;
|
||||
autoFocus: boolean;
|
||||
className?: string;
|
||||
isValid: boolean;
|
||||
onValueChange: (e: any) => void;
|
||||
}
|
||||
|
||||
const AutoFocusInputContainer = ({
|
||||
name,
|
||||
ariaLabel,
|
||||
placeholder,
|
||||
value,
|
||||
autoFocus,
|
||||
className,
|
||||
isValid,
|
||||
onValueChange,
|
||||
}: Props) => {
|
||||
const ref = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (autoFocus) {
|
||||
ref.current.focus();
|
||||
}
|
||||
}, [autoFocus]);
|
||||
|
||||
return (
|
||||
<TextBox
|
||||
ref={ref}
|
||||
type="text"
|
||||
className={`mb-0 pb-0 ${formClasses.validateField} ${
|
||||
isValid ? formClasses.invalidField : formClasses.validField
|
||||
} ${isValid ? 'is-invalid' : ''} ${className || ''}`}
|
||||
placeholder={placeholder}
|
||||
ariaLabel={ariaLabel}
|
||||
name={name}
|
||||
value={value}
|
||||
onChange={onValueChange}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
AutoFocusInputContainer.defaultProps = {
|
||||
className: undefined,
|
||||
};
|
||||
|
||||
const AutoFocusInputContainerMemo = memo(AutoFocusInputContainer, areEqual);
|
||||
|
||||
export { AutoFocusInputContainerMemo as AutoFocusInputContainer };
|
||||
@@ -0,0 +1 @@
|
||||
export { AutoFocusInputContainer as AutoFocusInput } from './AutoFocusInput';
|
||||
@@ -0,0 +1,59 @@
|
||||
import React, { memo, useEffect, useRef } from 'react';
|
||||
import { areEqual } from 'Utils/equalityChecks';
|
||||
|
||||
import formClasses from 'Themes/form/form.module.css';
|
||||
import { TextArea } from 'Components/TextArea';
|
||||
|
||||
interface Props {
|
||||
name: string;
|
||||
ariaLabel: string;
|
||||
placeholder: string;
|
||||
value: string;
|
||||
autoFocus: boolean;
|
||||
className?: string;
|
||||
isValid: boolean;
|
||||
onValueChange: (e: any) => void;
|
||||
}
|
||||
|
||||
const AutoFocusTextAreaContainer = ({
|
||||
name,
|
||||
ariaLabel,
|
||||
placeholder,
|
||||
value,
|
||||
autoFocus,
|
||||
className,
|
||||
isValid,
|
||||
onValueChange,
|
||||
}: Props) => {
|
||||
const ref = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (autoFocus) {
|
||||
ref.current.focus();
|
||||
}
|
||||
}, [autoFocus]);
|
||||
|
||||
return (
|
||||
<TextArea
|
||||
ref={ref}
|
||||
minRows={1}
|
||||
maxRows={6}
|
||||
className={`mb-0 pb-0 ${formClasses.validateField} ${
|
||||
isValid ? formClasses.invalidField : formClasses.validField
|
||||
} ${isValid ? 'is-invalid' : ''} ${className || ''}`}
|
||||
placeholder={placeholder}
|
||||
ariaLabel={ariaLabel}
|
||||
name={name}
|
||||
value={value}
|
||||
onChange={onValueChange}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
AutoFocusTextAreaContainer.defaultProps = {
|
||||
className: undefined,
|
||||
};
|
||||
|
||||
const AutoFocusTextAreaContainerMemo = memo(AutoFocusTextAreaContainer, areEqual);
|
||||
|
||||
export { AutoFocusTextAreaContainerMemo as AutoFocusTextAreaContainer };
|
||||
@@ -0,0 +1 @@
|
||||
export { AutoFocusTextAreaContainer as AutoFocusTextArea } from './AutoFocusTextArea';
|
||||
@@ -0,0 +1,17 @@
|
||||
import React, { memo, useState, useCallback } from 'react';
|
||||
|
||||
import { areEqual } from 'Utils/equalityChecks';
|
||||
import { ProjectScreenButton } from 'Components/Button';
|
||||
|
||||
const ProjectScreenButtonContainer = () => {
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
|
||||
const setModalStatus = useCallback(() => {
|
||||
setIsModalOpen((isModalOpen) => !isModalOpen);
|
||||
}, []);
|
||||
|
||||
return <ProjectScreenButton onClick={setModalStatus} isModalOpen={isModalOpen} setModalStatus={setModalStatus} />;
|
||||
};
|
||||
|
||||
const ProjectScreenButtonContainerMemo = memo(ProjectScreenButtonContainer, areEqual);
|
||||
export { ProjectScreenButtonContainerMemo as ProjectScreenButton };
|
||||
@@ -0,0 +1 @@
|
||||
export { ProjectScreenButton } from './ProjectScreenButton';
|
||||
@@ -0,0 +1,247 @@
|
||||
import { handleApiRequest } from 'Utils/handleApiRequest';
|
||||
|
||||
export const COMPANY_UPDATED = 'COMPANY_UPDATED';
|
||||
export const COMPANY_PHONES = 'COMPANY_PHONES';
|
||||
export const COMPANY_PHONE = 'COMPANY_PHONE';
|
||||
export const COMPANY_PHONE_CREATED = 'COMPANY_PHONE_CREATED';
|
||||
export const COMPANY_PHONE_UPDATED = 'COMPANY_PHONE_UPDATED';
|
||||
export const COMPANY_ADDRESS = 'COMPANY_ADDRESS';
|
||||
export const COMPANY_ADDRESS_UPDATED = 'COMPANY_ADDRESS_UPDATED';
|
||||
export const COMPANY_ADDRESS_CREATED = 'COMPANY_ADDRESS_CREATED';
|
||||
export const COMPANY_UPDATE_ERRORS = 'COMPANY_UPDATE_ERRORS';
|
||||
export const COMPANY_PHONE_UPDATE_ERRORS = 'COMPANY_PHONE_UPDATE_ERRORS';
|
||||
export const COMPANY_ADDRESS_UPDATE_ERRORS = 'COMPANY_ADDRESS_UPDATE_ERRORS';
|
||||
export const SET_LOGO_UPLOADING = 'SET_LOGO_UPLOADING';
|
||||
export const REFRESH_LOGO = 'REFRESH_LOGO';
|
||||
export const COMPANY_PHOTO_CATEGORIES = 'COMPANY_PHOTO_CATEGORIES';
|
||||
export const FETCHING_COMPANY_PHOTO_CATEGORIES = 'FETCHING_COMPANY_PHOTO_CATEGORIES';
|
||||
export const COMPANY_PHOTO_CATEGORIES_UPDATED = 'COMPANY_PHOTO_CATEGORIES_UPDATED';
|
||||
|
||||
interface ActionTypes {
|
||||
COMPANY_UPDATED: boolean;
|
||||
COMPANY_PHONES: any;
|
||||
COMPANY_PHONE: any;
|
||||
COMPANY_PHONE_CREATED: boolean;
|
||||
COMPANY_PHONE_UPDATED: boolean;
|
||||
COMPANY_ADDRESS: any;
|
||||
COMPANY_ADDRESS_CREATED: boolean;
|
||||
COMPANY_ADDRESS_UPDATED: boolean;
|
||||
COMPANY_UPDATE_ERRORS: any;
|
||||
COMPANY_PHONE_UPDATE_ERRORS: any;
|
||||
COMPANY_ADDRESS_UPDATE_ERRORS: any;
|
||||
SET_LOGO_UPLOADING: boolean;
|
||||
REFRESH_LOGO: boolean;
|
||||
COMPANY_PHOTO_CATEGORIES: any[];
|
||||
COMPANY_PHOTO_CATEGORIES_UPDATED: boolean;
|
||||
FETCHING_COMPANY_PHOTO_CATEGORIES: boolean;
|
||||
}
|
||||
|
||||
interface MessageAction {
|
||||
type: keyof ActionTypes;
|
||||
payload: any;
|
||||
}
|
||||
|
||||
export type SetCompanyTypes = MessageAction;
|
||||
|
||||
export const setCompanyUpdated = (value: boolean) => (dispatch) => {
|
||||
dispatch({
|
||||
type: COMPANY_UPDATED,
|
||||
payload: value,
|
||||
});
|
||||
};
|
||||
|
||||
export const setCompanyPhone = (value: any) => (dispatch) => {
|
||||
dispatch({
|
||||
type: COMPANY_PHONE,
|
||||
payload: value,
|
||||
});
|
||||
};
|
||||
|
||||
export const setCompanyPhoneCreated = (value: boolean) => (dispatch) => {
|
||||
dispatch({
|
||||
type: COMPANY_PHONE_CREATED,
|
||||
payload: value,
|
||||
});
|
||||
};
|
||||
|
||||
export const setCompanyPhoneUpdated = (value: boolean) => (dispatch) => {
|
||||
dispatch({
|
||||
type: COMPANY_PHONE_UPDATED,
|
||||
payload: value,
|
||||
});
|
||||
};
|
||||
|
||||
export const setCompanyAddress = (value: any) => (dispatch) => {
|
||||
dispatch({
|
||||
type: COMPANY_ADDRESS,
|
||||
payload: value,
|
||||
});
|
||||
};
|
||||
|
||||
export const setCompanyAddressCreated = (value: boolean) => (dispatch) => {
|
||||
dispatch({
|
||||
type: COMPANY_ADDRESS_CREATED,
|
||||
payload: value,
|
||||
});
|
||||
};
|
||||
|
||||
export const setCompanyAddressUpdated = (value: boolean) => (dispatch) => {
|
||||
dispatch({
|
||||
type: COMPANY_ADDRESS_UPDATED,
|
||||
payload: value,
|
||||
});
|
||||
};
|
||||
|
||||
export const setLogoUploading = (payload: any) => (dispatch) => {
|
||||
dispatch({
|
||||
type: SET_LOGO_UPLOADING,
|
||||
payload,
|
||||
});
|
||||
};
|
||||
|
||||
export const resetLogoUploading = (payload: any) => (dispatch) => {
|
||||
dispatch({
|
||||
type: SET_LOGO_UPLOADING,
|
||||
payload,
|
||||
});
|
||||
};
|
||||
|
||||
export const setCompanyPhotoCategoriesUpdated = (payload: boolean) => (dispatch) => {
|
||||
dispatch({
|
||||
type: COMPANY_PHOTO_CATEGORIES_UPDATED,
|
||||
payload,
|
||||
});
|
||||
};
|
||||
|
||||
/* eslint-disable */
|
||||
export const getCompanyPhones =
|
||||
(companyId: number) =>
|
||||
async (dispatch: any, _getState = null, utils: any) => {
|
||||
const response = await handleApiRequest(dispatch, utils.Api.get(`companies/${companyId}/phones`));
|
||||
|
||||
if (response?.data) {
|
||||
const { data } = response;
|
||||
dispatch({
|
||||
type: COMPANY_PHONES,
|
||||
payload: data,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const createCompanyPhone =
|
||||
(companyId: number, requestData) =>
|
||||
async (dispatch: any, _getState = null, utils: any) => {
|
||||
const response = await handleApiRequest(
|
||||
dispatch,
|
||||
utils.Api.post(`companies/${companyId}/phones`, requestData),
|
||||
COMPANY_PHONE_UPDATE_ERRORS
|
||||
);
|
||||
|
||||
if (response?.data) {
|
||||
const { data } = response;
|
||||
dispatch(setCompanyPhone(data));
|
||||
dispatch(setCompanyPhoneCreated(true));
|
||||
}
|
||||
};
|
||||
|
||||
export const updateCompanyPhone =
|
||||
(phoneId: number, requestData = {}) =>
|
||||
async (dispatch: any, _getState = null, utils: any) => {
|
||||
const response = await handleApiRequest(
|
||||
dispatch,
|
||||
utils.Api.put(`phones/${phoneId}`, requestData),
|
||||
COMPANY_PHONE_UPDATE_ERRORS
|
||||
);
|
||||
|
||||
if (response?.data) {
|
||||
const { data } = response;
|
||||
dispatch(setCompanyPhone(data));
|
||||
dispatch(setCompanyPhoneUpdated(true));
|
||||
}
|
||||
};
|
||||
|
||||
export const createCompanyAddress =
|
||||
(companyId: number, requestData) =>
|
||||
async (dispatch: any, _getState = null, utils: any) => {
|
||||
const response = await handleApiRequest(
|
||||
dispatch,
|
||||
utils.Api.post(`companies/${companyId}/addresses`, requestData),
|
||||
COMPANY_ADDRESS_UPDATE_ERRORS
|
||||
);
|
||||
|
||||
if (response?.data) {
|
||||
const { data } = response;
|
||||
dispatch(setCompanyAddress(data));
|
||||
dispatch(setCompanyAddressCreated(true));
|
||||
}
|
||||
};
|
||||
|
||||
export const updateCompanyAddress =
|
||||
(addressId: number, requestData = {}) =>
|
||||
async (dispatch: any, _getState = null, utils: any) => {
|
||||
const response = await handleApiRequest(
|
||||
dispatch,
|
||||
utils.Api.put(`addresses/${addressId}`, requestData),
|
||||
COMPANY_ADDRESS_UPDATE_ERRORS
|
||||
);
|
||||
|
||||
if (response?.data) {
|
||||
const { data } = response;
|
||||
|
||||
dispatch(setCompanyAddress(data));
|
||||
dispatch(setCompanyAddressUpdated(true));
|
||||
}
|
||||
};
|
||||
|
||||
export const updateCompanyDetails =
|
||||
(companyId: number, requestData = {}) =>
|
||||
async (dispatch: any, _getState = null, utils: any) => {
|
||||
const response = await handleApiRequest(
|
||||
dispatch,
|
||||
utils.Api.put(`companies/${companyId}`, requestData),
|
||||
COMPANY_UPDATE_ERRORS
|
||||
);
|
||||
|
||||
if (response?.data) {
|
||||
if (requestData) {
|
||||
dispatch(setCompanyUpdated(true));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const getCompanyPhotoCategories =
|
||||
(companyId: number) =>
|
||||
async (dispatch: any, _getState = null, utils: any) => {
|
||||
const response = await handleApiRequest(
|
||||
dispatch,
|
||||
utils.Api.get(`companies/${companyId}/albums`),
|
||||
'',
|
||||
FETCHING_COMPANY_PHOTO_CATEGORIES
|
||||
);
|
||||
|
||||
if (response?.data) {
|
||||
const { data } = response;
|
||||
dispatch({
|
||||
type: COMPANY_PHOTO_CATEGORIES,
|
||||
payload: data,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const updateCompanyPhotoCategories =
|
||||
(categories: any[], setFetching: any) =>
|
||||
async (dispatch: any, _getState = null, utils: any) => {
|
||||
setFetching(true);
|
||||
return await Promise.all(
|
||||
categories.map((category) => handleApiRequest(dispatch, utils.Api.put(`albums/${category.id}`, category)))
|
||||
)
|
||||
.then(() => {
|
||||
dispatch(setCompanyPhotoCategoriesUpdated(true));
|
||||
setFetching(false);
|
||||
return true;
|
||||
})
|
||||
.catch(() => {
|
||||
setFetching(false);
|
||||
return false;
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { companyReducer } from './reducers';
|
||||
@@ -0,0 +1,128 @@
|
||||
// types
|
||||
import {
|
||||
SetCompanyTypes,
|
||||
COMPANY_UPDATED,
|
||||
COMPANY_PHONES,
|
||||
COMPANY_PHONE,
|
||||
COMPANY_PHONE_UPDATED,
|
||||
COMPANY_PHONE_CREATED,
|
||||
COMPANY_ADDRESS,
|
||||
COMPANY_ADDRESS_CREATED,
|
||||
COMPANY_ADDRESS_UPDATED,
|
||||
COMPANY_UPDATE_ERRORS,
|
||||
COMPANY_PHONE_UPDATE_ERRORS,
|
||||
COMPANY_ADDRESS_UPDATE_ERRORS,
|
||||
SET_LOGO_UPLOADING,
|
||||
REFRESH_LOGO,
|
||||
COMPANY_PHOTO_CATEGORIES,
|
||||
COMPANY_PHOTO_CATEGORIES_UPDATED,
|
||||
FETCHING_COMPANY_PHOTO_CATEGORIES,
|
||||
} from 'Containers/Company/actions';
|
||||
|
||||
// state
|
||||
const initialState = {
|
||||
companyUpdated: false,
|
||||
companyPhones: [],
|
||||
companyPhone: undefined,
|
||||
companyPhoneCreated: false,
|
||||
companyPhoneUpdated: false,
|
||||
companyAddress: undefined,
|
||||
companyAddressUpdated: false,
|
||||
companyAddressCreated: false,
|
||||
companyUpdateErrors: undefined,
|
||||
companyPhoneUpdateErrors: undefined,
|
||||
companyAddressUpdateErrors: undefined,
|
||||
logoUploading: false,
|
||||
refreshAvatar: false,
|
||||
fetchingPhotoCategories: false,
|
||||
photoCategories: [],
|
||||
photoCategoriesUpdated: false,
|
||||
};
|
||||
|
||||
export const companyReducer = (state = initialState, action: SetCompanyTypes) => {
|
||||
const { type, payload } = action;
|
||||
switch (type) {
|
||||
case COMPANY_UPDATED:
|
||||
return {
|
||||
...state,
|
||||
companyUpdated: payload,
|
||||
};
|
||||
case COMPANY_PHONES:
|
||||
return {
|
||||
...state,
|
||||
companyPhones: payload,
|
||||
};
|
||||
case COMPANY_PHONE:
|
||||
return {
|
||||
...state,
|
||||
companyPhone: payload,
|
||||
};
|
||||
case COMPANY_PHONE_UPDATED:
|
||||
return {
|
||||
...state,
|
||||
companyPhoneUpdated: payload,
|
||||
};
|
||||
case COMPANY_PHONE_CREATED:
|
||||
return {
|
||||
...state,
|
||||
companyPhoneCreated: payload,
|
||||
};
|
||||
case COMPANY_ADDRESS:
|
||||
return {
|
||||
...state,
|
||||
companyAddress: payload,
|
||||
};
|
||||
case COMPANY_ADDRESS_UPDATED:
|
||||
return {
|
||||
...state,
|
||||
companyAddressUpdated: payload,
|
||||
};
|
||||
case COMPANY_ADDRESS_CREATED:
|
||||
return {
|
||||
...state,
|
||||
companyAddressCreated: payload,
|
||||
};
|
||||
case COMPANY_UPDATE_ERRORS:
|
||||
return {
|
||||
...state,
|
||||
companyUpdateErrors: payload,
|
||||
};
|
||||
case COMPANY_PHONE_UPDATE_ERRORS:
|
||||
return {
|
||||
...state,
|
||||
companyPhoneUpdateErrors: payload,
|
||||
};
|
||||
case COMPANY_ADDRESS_UPDATE_ERRORS:
|
||||
return {
|
||||
...state,
|
||||
companyAddressUpdateErrors: payload,
|
||||
};
|
||||
case SET_LOGO_UPLOADING:
|
||||
return {
|
||||
...state,
|
||||
logoUploading: payload,
|
||||
};
|
||||
case REFRESH_LOGO:
|
||||
return {
|
||||
...state,
|
||||
refreshLogo: payload,
|
||||
};
|
||||
case FETCHING_COMPANY_PHOTO_CATEGORIES:
|
||||
return {
|
||||
...state,
|
||||
fetchingPhotoCategories: payload,
|
||||
};
|
||||
case COMPANY_PHOTO_CATEGORIES:
|
||||
return {
|
||||
...state,
|
||||
photoCategories: payload,
|
||||
};
|
||||
case COMPANY_PHOTO_CATEGORIES_UPDATED:
|
||||
return {
|
||||
...state,
|
||||
photoCategoriesUpdated: payload,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
export const companyPhonesSelector = ({ company: { companyPhones: value = [] } }: any) => value;
|
||||
|
||||
export const companyPhoneCreatedSelector = ({ company: { companyPhoneCreated: value = false } }: any) => value;
|
||||
export const companyAddressCreatedSelector = ({ company: { companyAddressCreated: value = false } }: any) => value;
|
||||
|
||||
export const companyUpdatedSelector = ({ company: { companyUpdated: value = false } }: any) => value;
|
||||
export const companyAddressUpdatedSelector = ({ company: { companyAddressUpdated: value = false } }: any) => value;
|
||||
export const companyPhoneUpdatedSelector = ({ company: { companyPhoneUpdated: value = false } }: any) => value;
|
||||
|
||||
export const companyLogoUploading = ({ company: { logoUploading: value = false } }) => value;
|
||||
export const fetchingPhotoCategoriesSelector = ({ company: { fetchingPhotoCategories: value = true } }) => value;
|
||||
export const photoCategoriesSelector = ({ company: { photoCategories: value = [] } }) => value;
|
||||
export const photoCategoriesUpdatedSelector = ({ company: { photoCategoriesUpdated: value = false } }) => value;
|
||||
|
||||
// form errors
|
||||
export const companyNameErrorSelector = ({ company: { companyUpdateErrors } }: any) => companyUpdateErrors?.name || [];
|
||||
export const companyPhoneErrorSelector = ({ company: { companyPhoneUpdateErrors } }: any) =>
|
||||
companyPhoneUpdateErrors?.value || [];
|
||||
export const companyWebsiteErrorSelector = ({ company: { companyUpdateErrors } }: any) =>
|
||||
companyUpdateErrors?.website || [];
|
||||
export const companyAddressErrorSelector = ({ company: { companyAddressUpdateErrors } }: any) =>
|
||||
companyAddressUpdateErrors?.address || [];
|
||||
export const companyAddressSecondErrorSelector = ({ company: { companyAddressUpdateErrors } }: any) =>
|
||||
companyAddressUpdateErrors?.address_2 || [];
|
||||
export const companyCountryErrorSelector = ({ company: { companyAddressUpdateErrors } }: any) =>
|
||||
companyAddressUpdateErrors?.country || [];
|
||||
export const companyStateErrorSelector = ({ company: { companyAddressUpdateErrors } }: any) =>
|
||||
companyAddressUpdateErrors?.state || [];
|
||||
export const companyCityErrorSelector = ({ company: { companyAddressUpdateErrors } }: any) =>
|
||||
companyAddressUpdateErrors?.city || [];
|
||||
export const companyCodeErrorSelector = ({ company: { companyAddressUpdateErrors } }: any) =>
|
||||
companyAddressUpdateErrors?.zip || [];
|
||||
@@ -0,0 +1,86 @@
|
||||
// types
|
||||
|
||||
export const SET_FETCHING = 'SET_FETCHING';
|
||||
export const SET_REDIRECT_PATH = 'SET_REDIRECT_PATH';
|
||||
export const SET_TOASTER = 'SET_TOASTER';
|
||||
export const SIDE_BAR = 'SIDE_BAR';
|
||||
export const FORM_ERRORS = 'FORM_ERRORS';
|
||||
export const RESET_TOAST = 'RESET_TOAST';
|
||||
export const APP_INITIAL_LOADING = 'APP_INITIAL_LOADING';
|
||||
export const SET_PUSHER = 'SET_PUSHER';
|
||||
|
||||
interface ActionTypes {
|
||||
SET_FETCHING: boolean;
|
||||
SET_REDIRECT_PATH: string;
|
||||
SET_TOASTER: object;
|
||||
SIDE_BAR: boolean;
|
||||
FORM_ERRORS: any;
|
||||
RESET_TOAST: undefined;
|
||||
APP_INITIAL_LOADING: boolean;
|
||||
SET_PUSHER: any;
|
||||
}
|
||||
|
||||
interface MessageAction {
|
||||
type: keyof ActionTypes;
|
||||
payload: any;
|
||||
}
|
||||
|
||||
export type SetCoreTypes = MessageAction;
|
||||
|
||||
export const setAppInitialLoading = (payload: any) => (dispatch) => {
|
||||
dispatch({
|
||||
type: APP_INITIAL_LOADING,
|
||||
payload,
|
||||
});
|
||||
};
|
||||
|
||||
export const setAppRedirectPathLocal = (value: string) => localStorage.setItem('appRedirectPath', value);
|
||||
export const getAppRedirectPathLocal = () => localStorage.getItem('appRedirectPath');
|
||||
export const removeAppRedirectPathLocal = () => localStorage.removeItem('appRedirectPath');
|
||||
|
||||
// TODO::ADD THUNKS FOR DIFFERENT TYPES OF SPINNERS
|
||||
|
||||
export const setFetching =
|
||||
(payload: boolean, type = 'SET_FETCHING') =>
|
||||
(dispatch: any) => {
|
||||
dispatch({
|
||||
type,
|
||||
payload,
|
||||
});
|
||||
};
|
||||
|
||||
export const setToaster =
|
||||
(message: string, success = true, timeout = 3000) =>
|
||||
(dispatch: any) => {
|
||||
dispatch({
|
||||
type: SET_TOASTER,
|
||||
payload: {
|
||||
show: true,
|
||||
message,
|
||||
success,
|
||||
timeout,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const resetToaster = () => async (dispatch: any) => {
|
||||
dispatch({
|
||||
type: RESET_TOAST,
|
||||
});
|
||||
};
|
||||
|
||||
export const setFormErrors =
|
||||
(errors: any, type = 'FORM_ERRORS') =>
|
||||
(dispatch: any) => {
|
||||
dispatch({
|
||||
type: type || FORM_ERRORS,
|
||||
payload: errors,
|
||||
});
|
||||
};
|
||||
|
||||
export const setPusher = (payload: any) => (dispatch) => {
|
||||
dispatch({
|
||||
type: SET_PUSHER,
|
||||
payload,
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,2 @@
|
||||
export { CoreReducer } from "./reducer";
|
||||
export { setToaster } from "./actions";
|
||||
@@ -0,0 +1,85 @@
|
||||
// types
|
||||
import {
|
||||
SET_FETCHING,
|
||||
SET_REDIRECT_PATH,
|
||||
SET_TOASTER,
|
||||
SIDE_BAR,
|
||||
FORM_ERRORS,
|
||||
RESET_TOAST,
|
||||
SetCoreTypes,
|
||||
APP_INITIAL_LOADING,
|
||||
SET_PUSHER,
|
||||
} from 'Containers/Core/actions';
|
||||
|
||||
// state
|
||||
const initialState = {
|
||||
fetching: false,
|
||||
redirectPath: '/',
|
||||
toast: {
|
||||
show: false,
|
||||
message: '',
|
||||
icon: false,
|
||||
success: true,
|
||||
timeout: 3000,
|
||||
},
|
||||
sideBar: false,
|
||||
formErrors: {},
|
||||
appInitialLoading: true,
|
||||
pusher: undefined,
|
||||
};
|
||||
|
||||
// we we'll use this for system variables, etc. loaders
|
||||
export const CoreReducer = (state = initialState, action: SetCoreTypes) => {
|
||||
const { type, payload } = action;
|
||||
|
||||
switch (type) {
|
||||
case SET_FETCHING:
|
||||
return {
|
||||
...state,
|
||||
fetching: payload,
|
||||
};
|
||||
case SET_REDIRECT_PATH:
|
||||
return {
|
||||
...state,
|
||||
redirectPath: payload,
|
||||
};
|
||||
case SET_TOASTER:
|
||||
return {
|
||||
...state,
|
||||
toast: {
|
||||
...state.toast,
|
||||
...payload,
|
||||
},
|
||||
};
|
||||
case SIDE_BAR:
|
||||
return {
|
||||
...state,
|
||||
sideBar: payload,
|
||||
};
|
||||
case FORM_ERRORS:
|
||||
return {
|
||||
...state,
|
||||
formErrors:
|
||||
typeof payload === 'object'
|
||||
? {
|
||||
...payload,
|
||||
}
|
||||
: payload,
|
||||
};
|
||||
case APP_INITIAL_LOADING:
|
||||
return {
|
||||
...state,
|
||||
appInitialLoading: payload,
|
||||
};
|
||||
case SET_PUSHER:
|
||||
return {
|
||||
...state,
|
||||
pusher: payload,
|
||||
};
|
||||
case RESET_TOAST: {
|
||||
return initialState;
|
||||
}
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
export const appInitialLoadingSelector = ({ core: { appInitialLoading: value = true } }: any) => value;
|
||||
export const appRedirectPathSelector = ({ core: { appRedirectPath: value } }: any) => value;
|
||||
export const coreFetchingSelector = ({ core: { fetching = false } }: any) => fetching;
|
||||
export const pusherSelector = ({ core: { pusher: value } }: any) => value;
|
||||
@@ -0,0 +1,143 @@
|
||||
import React, { memo, useCallback, useEffect, useState } from 'react';
|
||||
import { areEqual } from 'Utils/equalityChecks';
|
||||
import { useUser } from 'Context/User';
|
||||
import { UserModel } from 'Containers/User/Models/UserModel';
|
||||
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import {
|
||||
employeeRemovedSelector,
|
||||
employeesAttachedSelector,
|
||||
fetchingMembersSelector,
|
||||
membersSelector,
|
||||
} from 'Containers/Crew/selectors';
|
||||
import { listProjectMembers, setEmployeesAttached } from 'Containers/Crew/actions';
|
||||
import { projectIdSelector } from 'Containers/RocketScan/selectors';
|
||||
|
||||
import { Spinner } from 'Components/Spinner';
|
||||
import { GeneralToast } from 'Components/Toast';
|
||||
import { AvatarOrInitials } from 'Components/Avatar';
|
||||
import { CrewPlaceholder } from 'Components/Crew/CrewPlaceholder';
|
||||
import { SelectMembersModal, Members } from 'Containers/Crew';
|
||||
import { CrewWrapper, Crew } from 'Components/Crew';
|
||||
|
||||
import { convertWordsFirstLetterUppercase } from 'Utils/helpers';
|
||||
import classes from './crew.module.css';
|
||||
|
||||
const CrewContainer = () => {
|
||||
const {
|
||||
avatar_url: avatar = '',
|
||||
first_name: firstName,
|
||||
last_name: lastName,
|
||||
full_name: fullName,
|
||||
}: UserModel = useUser();
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
// user object
|
||||
const { id: userId } = useUser();
|
||||
|
||||
// local variables
|
||||
const [isOpenSelectMembersModal, setIsOpenSelectMembersModal] = useState(false);
|
||||
const [showToast, setShowToast] = useState(false);
|
||||
const [toastMessage, setToastMessage] = useState('');
|
||||
|
||||
// selectors
|
||||
const employeesAttached = useSelector(employeesAttachedSelector, areEqual);
|
||||
const employeeRemoved = useSelector(employeeRemovedSelector, areEqual);
|
||||
const members = useSelector(membersSelector, areEqual);
|
||||
const projectId = useSelector(projectIdSelector, areEqual);
|
||||
const fetching = useSelector(fetchingMembersSelector, areEqual);
|
||||
|
||||
// api
|
||||
const getProjectMembers = useCallback(() => {
|
||||
dispatch(listProjectMembers(projectId, userId));
|
||||
}, [projectId]);
|
||||
|
||||
// initial api call
|
||||
useEffect(() => {
|
||||
if (projectId) {
|
||||
getProjectMembers();
|
||||
}
|
||||
}, [projectId]);
|
||||
|
||||
const onClickAddCrew = useCallback(() => {
|
||||
setIsOpenSelectMembersModal(true);
|
||||
}, []);
|
||||
|
||||
const onClickMemberModalCloseClick = useCallback((e: any) => {
|
||||
e.preventDefault();
|
||||
setIsOpenSelectMembersModal(false);
|
||||
}, []);
|
||||
|
||||
const onClickToastClose = useCallback(() => setShowToast(false), []);
|
||||
|
||||
useEffect(() => {
|
||||
if (employeesAttached) {
|
||||
setIsOpenSelectMembersModal(false);
|
||||
setToastMessage('Crew member(s) added');
|
||||
setShowToast(true);
|
||||
|
||||
setTimeout(() => {
|
||||
setShowToast(false);
|
||||
}, 2500);
|
||||
}
|
||||
}, [employeesAttached]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpenSelectMembersModal && employeesAttached) {
|
||||
getProjectMembers();
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (!isOpenSelectMembersModal && employeesAttached) {
|
||||
dispatch(setEmployeesAttached(false));
|
||||
}
|
||||
};
|
||||
}, [isOpenSelectMembersModal, employeesAttached]);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof employeeRemoved === 'string') {
|
||||
dispatch(listProjectMembers(projectId, userId));
|
||||
|
||||
setToastMessage(`${convertWordsFirstLetterUppercase(employeeRemoved)} Removed From Project`);
|
||||
setShowToast(true);
|
||||
|
||||
setTimeout(() => {
|
||||
setShowToast(false);
|
||||
}, 2500);
|
||||
}
|
||||
}, [employeeRemoved]);
|
||||
|
||||
return (
|
||||
<CrewWrapper>
|
||||
<Crew
|
||||
onClickAddCrew={onClickAddCrew}
|
||||
hasCrew={members.length > 0}
|
||||
currentUserAvatar={<AvatarOrInitials avatar={avatar || ''} firstName={firstName} lastName={lastName} />}
|
||||
currentUserName={fullName}
|
||||
>
|
||||
<Spinner loading={fetching} />
|
||||
|
||||
{members.length === 0 && !fetching && <CrewPlaceholder onClickAddCrew={onClickAddCrew} />}
|
||||
|
||||
{!fetching && (
|
||||
<SelectMembersModal isOpen={isOpenSelectMembersModal} onClickModalCloseClick={onClickMemberModalCloseClick} />
|
||||
)}
|
||||
|
||||
{members.length > 0 && <Members members={members} />}
|
||||
|
||||
<GeneralToast
|
||||
id="crew-toast"
|
||||
className={classes.toast}
|
||||
show={showToast}
|
||||
message={toastMessage}
|
||||
closeToast={onClickToastClose}
|
||||
/>
|
||||
</Crew>
|
||||
</CrewWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
const CrewContainerMemo = memo(CrewContainer, areEqual);
|
||||
|
||||
export { CrewContainerMemo as Crew };
|
||||
@@ -0,0 +1,3 @@
|
||||
.toast {
|
||||
width: calc(100% - 339px);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { Crew } from './Crew';
|
||||
@@ -0,0 +1,57 @@
|
||||
import React, { memo, useCallback } from 'react';
|
||||
import { areEqual } from 'Utils/equalityChecks';
|
||||
|
||||
import { UserModel } from 'Containers/User/Models/UserModel';
|
||||
import { EmployeeCard } from 'Components/People/PeopleTabs/EmployeeCard';
|
||||
import { AvatarOrInitials } from 'Components/Avatar';
|
||||
import { formatPhoneInternationalWithCountryCode } from 'Utils/helpers';
|
||||
|
||||
interface Props {
|
||||
member: UserModel;
|
||||
onClickMember: (e: any) => void;
|
||||
}
|
||||
|
||||
const MemberContainer = ({ member, onClickMember }: Props) => {
|
||||
const {
|
||||
id,
|
||||
first_name: firstName,
|
||||
last_name: lastName,
|
||||
full_name: fullName,
|
||||
avatar_url: avatar,
|
||||
email,
|
||||
phones,
|
||||
roles,
|
||||
} = member;
|
||||
|
||||
let formattedPhoneNumber;
|
||||
let extension;
|
||||
if (phones.length > 0) {
|
||||
const [phone] = phones;
|
||||
|
||||
const { value: phoneNumber, country_code: code } = phone;
|
||||
|
||||
extension = phone.extension;
|
||||
formattedPhoneNumber = formatPhoneInternationalWithCountryCode(code, phoneNumber);
|
||||
}
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
onClickMember(member);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<EmployeeCard
|
||||
id={id.toString()}
|
||||
avatar={<AvatarOrInitials avatar={avatar || ''} firstName={firstName} lastName={lastName} />}
|
||||
name={fullName}
|
||||
email={email}
|
||||
phone={formattedPhoneNumber}
|
||||
extension={extension}
|
||||
roles={roles}
|
||||
selectCardClick={onClick}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const MemberContainerMemo = memo(MemberContainer, areEqual);
|
||||
|
||||
export { MemberContainerMemo as MemberContainer };
|
||||
@@ -0,0 +1 @@
|
||||
export { MemberContainer as Member } from './Member';
|
||||
@@ -0,0 +1,153 @@
|
||||
import React, { memo, useCallback, useEffect, useState } from 'react';
|
||||
import { areEqual } from 'Utils/equalityChecks';
|
||||
import { UserModel } from 'Containers/User/Models/UserModel';
|
||||
import { countries } from 'Utils/data';
|
||||
|
||||
import { Member } from 'Containers/Crew';
|
||||
import { RemoveMemberModal } from 'Components/Crew/RemoveMemberModal';
|
||||
import { AvatarOrInitials } from 'Components/Avatar';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { listProjectMembers, removeEmployeeFromProject, setEmployeeRemoved } from 'Containers/Crew/actions';
|
||||
import { projectIdSelector } from 'Containers/RocketScan/selectors';
|
||||
import { employeeRemovedSelector, removingEmployeeSelector } from 'Containers/Crew/selectors';
|
||||
import { useUser } from 'Context/User';
|
||||
import { formatPhoneNumberInternational } from 'Utils/helpers';
|
||||
import classes from './members.module.css';
|
||||
|
||||
interface Props {
|
||||
members: Array<UserModel>;
|
||||
}
|
||||
|
||||
const MembersContainer = ({ members }: Props) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
// user object
|
||||
const { id: userId } = useUser();
|
||||
const user: UserModel = useUser();
|
||||
|
||||
const [isOpenModal, setIsOpenModal] = useState(false);
|
||||
const [id, setId] = useState('');
|
||||
const [name, setName] = useState('');
|
||||
const [avatar, setAvatar] = useState('');
|
||||
const [firstName, setFirstName] = useState('');
|
||||
const [lastName, setLastName] = useState('');
|
||||
const [email, setEmail] = useState('');
|
||||
const [phone, setPhone] = useState('');
|
||||
const [extension, setExtension] = useState('');
|
||||
const [countryFlag, setCountryFlag] = useState('usa');
|
||||
const [roles, setRoles] = useState([]);
|
||||
|
||||
const projectId = useSelector(projectIdSelector, areEqual);
|
||||
const employeeRemoved = useSelector(employeeRemovedSelector, areEqual);
|
||||
const fetching = useSelector(removingEmployeeSelector, areEqual);
|
||||
|
||||
const onClickMember = useCallback(
|
||||
({
|
||||
id,
|
||||
first_name: firstName,
|
||||
last_name: lastName,
|
||||
full_name: fullName,
|
||||
avatar_url: avatar,
|
||||
email,
|
||||
phones,
|
||||
roles,
|
||||
}: UserModel) => {
|
||||
setId(id.toString());
|
||||
setName(fullName);
|
||||
setAvatar(avatar);
|
||||
setFirstName(firstName);
|
||||
setLastName(lastName);
|
||||
setEmail(email);
|
||||
const [firstPhone] = phones;
|
||||
const { value, extension } = firstPhone;
|
||||
setPhone(formatPhoneNumberInternational(value));
|
||||
setExtension(extension);
|
||||
setRoles(roles);
|
||||
|
||||
setIsOpenModal(true);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const onModalCloseClick = useCallback((e: any) => {
|
||||
e.preventDefault();
|
||||
setIsOpenModal(false);
|
||||
}, []);
|
||||
|
||||
const onClickRemoveProject = useCallback(() => {
|
||||
dispatch(removeEmployeeFromProject(projectId, id));
|
||||
}, [projectId, id]);
|
||||
|
||||
useEffect(() => {
|
||||
if (employeeRemoved) {
|
||||
setIsOpenModal(false);
|
||||
}
|
||||
}, [employeeRemoved]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpenModal && employeeRemoved) {
|
||||
dispatch(listProjectMembers(projectId, userId));
|
||||
dispatch(setEmployeeRemoved(name));
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (!isOpenModal && employeeRemoved) {
|
||||
dispatch(setEmployeeRemoved(undefined));
|
||||
}
|
||||
};
|
||||
}, [isOpenModal, employeeRemoved]);
|
||||
|
||||
// get country code and flag
|
||||
useEffect(() => {
|
||||
if (user?.id) {
|
||||
const { companies } = user;
|
||||
if (companies.length > 0) {
|
||||
const [company] = companies;
|
||||
const { country_alpha_2: countryAlphaTwo } = company;
|
||||
|
||||
const countryAlpha = countryAlphaTwo;
|
||||
const companyCountry = countries.find((country) => country.alpha_2 === countryAlpha);
|
||||
|
||||
if (companyCountry?.id) {
|
||||
const { flag } = companyCountry;
|
||||
setCountryFlag(flag);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [user, phone]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={classes.membersBase}>
|
||||
{members.map((member) => (
|
||||
<Member key={member.id} member={member} onClickMember={onClickMember} />
|
||||
))}
|
||||
</div>
|
||||
<RemoveMemberModal
|
||||
id={id}
|
||||
name={name}
|
||||
flag={countryFlag}
|
||||
phone={phone}
|
||||
email={email}
|
||||
extension={extension}
|
||||
roles={roles}
|
||||
avatar={
|
||||
<AvatarOrInitials
|
||||
avatarClassName={classes.avatar}
|
||||
avatar={avatar || ''}
|
||||
firstName={firstName}
|
||||
lastName={lastName}
|
||||
/>
|
||||
}
|
||||
isOpen={isOpenModal}
|
||||
fetching={fetching}
|
||||
modalCloseClick={onModalCloseClick}
|
||||
onClickRemoveProject={onClickRemoveProject}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const MembersContainerMemo = memo(MembersContainer, areEqual);
|
||||
|
||||
export { MembersContainerMemo as MembersContainer };
|
||||
@@ -0,0 +1 @@
|
||||
export { MembersContainer as Members } from './Members';
|
||||
@@ -0,0 +1,12 @@
|
||||
.membersBase {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
width: 100%;
|
||||
padding: 0 30px;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
margin-right: 24px;
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
import React, { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { areEqual } from 'Utils/equalityChecks';
|
||||
import { debounce } from 'Utils/debounce';
|
||||
import { addOrRemoveFromArray } from 'Utils/helpers';
|
||||
|
||||
import {
|
||||
attachingEmployeesSelector,
|
||||
employeesAttachedSelector,
|
||||
employeesSelector,
|
||||
fetchingCrewEmployeesSelector,
|
||||
} from 'Containers/Crew/selectors';
|
||||
import { listEmployees, setAttachingEmployees, syncEmployeeToProject } from 'Containers/Crew/actions';
|
||||
import { projectCompanyIdSelector, projectIdSelector } from 'Containers/RocketScan/selectors';
|
||||
|
||||
import { useUser } from 'Context/User';
|
||||
|
||||
import { Modal } from 'Components/Modal';
|
||||
import { SearchBox } from 'Components/SearchBox';
|
||||
import { EmployeeDirectory, EmployeesEmptyPlaceholder } from 'Components/Crew';
|
||||
import { Spinner } from 'Components/Spinner';
|
||||
|
||||
import classes from './selectMembersModal.module.css';
|
||||
|
||||
interface Props {
|
||||
isOpen: boolean;
|
||||
onClickModalCloseClick: (e: any) => void;
|
||||
}
|
||||
|
||||
const SelectMembersModalContainer = ({ isOpen, onClickModalCloseClick }: Props) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const textBoxRef = useRef(undefined);
|
||||
|
||||
// user object
|
||||
const { id: userId } = useUser();
|
||||
|
||||
// local variables
|
||||
const [searchValue, setSearchValue] = useState('');
|
||||
const [selectedMembers, setSelectedMembers] = useState([]);
|
||||
const [showNoEmployees, setShowNoEmployees] = useState(true);
|
||||
const [showError, setShowError] = useState(false);
|
||||
const [initialMount, setInitialMount] = useState(false);
|
||||
|
||||
// selectors
|
||||
const employees = useSelector(employeesSelector, areEqual);
|
||||
const companyId = useSelector(projectCompanyIdSelector, areEqual);
|
||||
const projectId = useSelector(projectIdSelector, areEqual);
|
||||
const fetching = useSelector(fetchingCrewEmployeesSelector, areEqual);
|
||||
const disableButton = useSelector(attachingEmployeesSelector, areEqual);
|
||||
const employeesAttached = useSelector(employeesAttachedSelector, areEqual);
|
||||
|
||||
useEffect(() => {
|
||||
setInitialMount(true);
|
||||
}, []);
|
||||
|
||||
// api call
|
||||
const getEmployees = useCallback(
|
||||
(search = '') => {
|
||||
dispatch(listEmployees(companyId, userId, search));
|
||||
},
|
||||
[companyId, userId]
|
||||
);
|
||||
|
||||
// initial api call
|
||||
useEffect(() => {
|
||||
if (isOpen && companyId && employees.length === 0) {
|
||||
getEmployees();
|
||||
}
|
||||
}, [isOpen, companyId]);
|
||||
|
||||
// handle search box value change
|
||||
const handleSearchValueChange = ({ target: { value } }: any) => {
|
||||
if (value.length <= 36) {
|
||||
setInitialMount(false);
|
||||
setSearchValue(value);
|
||||
if (value.length >= 2) {
|
||||
getEmployees(value);
|
||||
}
|
||||
if (value.length === 0) {
|
||||
getEmployees();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// show no employees only once on initial mount
|
||||
useEffect(() => {
|
||||
if (initialMount) {
|
||||
setShowNoEmployees(employees.length === 0);
|
||||
}
|
||||
}, [initialMount, employees]);
|
||||
|
||||
// debounce function on search value change
|
||||
const onChangeSearchValue = useMemo(() => debounce(handleSearchValueChange, 300), [companyId]);
|
||||
|
||||
const onClickClearButton = useCallback(() => {
|
||||
getEmployees();
|
||||
setSearchValue('');
|
||||
textBoxRef.current.value = '';
|
||||
textBoxRef.current.focus();
|
||||
setShowError(false);
|
||||
}, [companyId, userId, textBoxRef]);
|
||||
|
||||
// generate members id array
|
||||
const onClickMemberRow = useCallback(({ currentTarget: { id } }: any) => {
|
||||
setSelectedMembers((prevIds) => addOrRemoveFromArray(prevIds, id));
|
||||
}, []);
|
||||
|
||||
// form submit api call
|
||||
const onClickSelectCrew = useCallback(() => {
|
||||
setShowError(selectedMembers.length === 0);
|
||||
if (selectedMembers.length > 0) {
|
||||
dispatch(setAttachingEmployees(true));
|
||||
|
||||
// no bulk users submit so we use a loop to submit multiple members
|
||||
selectedMembers.forEach((selectedMember: string, index: number) =>
|
||||
dispatch(syncEmployeeToProject(projectId, selectedMember, selectedMembers.length - 1 === index))
|
||||
);
|
||||
}
|
||||
}, [selectedMembers, projectId]);
|
||||
|
||||
// refresh employees
|
||||
useEffect(() => {
|
||||
if (employeesAttached) {
|
||||
setSelectedMembers([]);
|
||||
getEmployees();
|
||||
}
|
||||
}, [employeesAttached]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
title="Select Crew"
|
||||
id="select-crew"
|
||||
modalCloseClick={onClickModalCloseClick}
|
||||
modalHeader
|
||||
classes={classes}
|
||||
>
|
||||
{!showNoEmployees && (
|
||||
<SearchBox
|
||||
ref={textBoxRef}
|
||||
id="crew-search"
|
||||
name="search"
|
||||
ariaLabel="Search a crew member"
|
||||
value={searchValue}
|
||||
onChangeValue={onChangeSearchValue}
|
||||
onClickClearButton={onClickClearButton}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!showNoEmployees && (
|
||||
<EmployeeDirectory
|
||||
employees={employees}
|
||||
selectedMembers={selectedMembers}
|
||||
showError={showError}
|
||||
searchValue={searchValue}
|
||||
disableButton={disableButton}
|
||||
onClickMemberRow={onClickMemberRow}
|
||||
onClickSelectCrew={onClickSelectCrew}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!fetching && showNoEmployees && <EmployeesEmptyPlaceholder />}
|
||||
|
||||
<Spinner loading={fetching} />
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
const SelectMembersModalContainerMemo = memo(SelectMembersModalContainer, areEqual);
|
||||
|
||||
export { SelectMembersModalContainerMemo as SelectMembersModalContainer };
|
||||
@@ -0,0 +1 @@
|
||||
export { SelectMembersModalContainer as SelectMembersModal } from './SelectMembersModal';
|
||||
@@ -0,0 +1,26 @@
|
||||
.modalTitle {
|
||||
font-family: IBM Plex Sans;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
font-size: 20px;
|
||||
line-height: 32px;
|
||||
text-align: center;
|
||||
color: #5B476B;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.modalHeader {
|
||||
text-align: center;
|
||||
border-bottom: none!important;
|
||||
}
|
||||
|
||||
.modalBody {
|
||||
margin: 0 24px;
|
||||
padding: 8px 0 24px;
|
||||
border-top: 1px solid #E8E7ED;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.modalDialog {
|
||||
max-width: 600px;
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
import { handleApiRequest } from 'Utils/handleApiRequest';
|
||||
|
||||
export const SET_CREW_EMPLOYEES = 'SET_CREW_EMPLOYEES';
|
||||
export const FETCHING_CREW_EMPLOYEES = 'FETCHING_CREW_EMPLOYEES';
|
||||
export const EMPLOYEES_ATTACHED = 'EMPLOYEES_ATTACHED';
|
||||
export const ATTACHING_EMPLOYEES = 'ATTACHING_EMPLOYEES';
|
||||
export const SET_PROJECT_MEMBERS = 'SET_PROJECT_MEMBERS';
|
||||
export const FETCHING_PROJECT_MEMBERS = 'FETCHING_PROJECT_MEMBERS';
|
||||
export const EMPLOYEE_REMOVED = 'EMPLOYEE_REMOVED';
|
||||
export const REMOVING_EMPLOYEE = 'REMOVING_EMPLOYEE';
|
||||
|
||||
interface ActionTypes {
|
||||
SET_CREW_EMPLOYEES: any;
|
||||
FETCHING_CREW_EMPLOYEES: boolean;
|
||||
EMPLOYEES_ATTACHED: boolean;
|
||||
ATTACHING_EMPLOYEES: boolean;
|
||||
SET_PROJECT_MEMBERS: any;
|
||||
FETCHING_PROJECT_MEMBERS: boolean;
|
||||
EMPLOYEE_REMOVED: boolean;
|
||||
REMOVING_EMPLOYEE: boolean;
|
||||
}
|
||||
|
||||
interface MessageAction {
|
||||
type: keyof ActionTypes;
|
||||
payload: any;
|
||||
}
|
||||
|
||||
export type SetCrewTypes = MessageAction;
|
||||
|
||||
/*
|
||||
* NON ASYNC THUNKS
|
||||
* */
|
||||
|
||||
export const setEmployeesAttached = (value: any) => (dispatch) => {
|
||||
dispatch({
|
||||
type: EMPLOYEES_ATTACHED,
|
||||
payload: value,
|
||||
});
|
||||
};
|
||||
|
||||
export const setAttachingEmployees = (value: any) => (dispatch) => {
|
||||
dispatch({
|
||||
type: ATTACHING_EMPLOYEES,
|
||||
payload: value,
|
||||
});
|
||||
};
|
||||
|
||||
export const setEmployeeRemoved = (value: any) => (dispatch) => {
|
||||
dispatch({
|
||||
type: EMPLOYEE_REMOVED,
|
||||
payload: value,
|
||||
});
|
||||
};
|
||||
|
||||
/* eslint-disable */
|
||||
export const listEmployees =
|
||||
(companyId: number, userId: number, search = '') =>
|
||||
async (dispatch: any, _getState = null, utils: any) => {
|
||||
const response = await handleApiRequest(
|
||||
dispatch,
|
||||
utils.Api.get(`companies/${companyId}/users`, {
|
||||
params: {
|
||||
'filter[search]': search,
|
||||
include: 'roles',
|
||||
},
|
||||
}),
|
||||
'',
|
||||
FETCHING_CREW_EMPLOYEES
|
||||
);
|
||||
|
||||
if (response?.data) {
|
||||
const { data } = response;
|
||||
dispatch({
|
||||
type: SET_CREW_EMPLOYEES,
|
||||
payload: {
|
||||
data,
|
||||
userId,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const syncEmployeeToProject =
|
||||
(projectId: number, userId: string, last: boolean) =>
|
||||
async (dispatch: any, _getState = null, utils: any) => {
|
||||
const response = await handleApiRequest(dispatch, utils.Api.post(`projects/${projectId}/users/${userId}`, {}));
|
||||
|
||||
if (typeof response === 'string') {
|
||||
if (last) {
|
||||
dispatch(setAttachingEmployees(false));
|
||||
dispatch(setEmployeesAttached(true));
|
||||
}
|
||||
} else {
|
||||
if (last) {
|
||||
dispatch(setAttachingEmployees(false));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const listProjectMembers =
|
||||
(projectId: number, userId: number, page = 1) =>
|
||||
async (dispatch: any, _getState = null, utils: any) => {
|
||||
const response = await handleApiRequest(
|
||||
dispatch,
|
||||
utils.Api.get(`projects/${projectId}/users`, {
|
||||
params: {
|
||||
limit: 100,
|
||||
page,
|
||||
include: 'roles',
|
||||
},
|
||||
}),
|
||||
'',
|
||||
FETCHING_PROJECT_MEMBERS
|
||||
);
|
||||
|
||||
if (response?.data) {
|
||||
const { data } = response;
|
||||
|
||||
dispatch({
|
||||
type: SET_PROJECT_MEMBERS,
|
||||
payload: {
|
||||
data,
|
||||
userId,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const removeEmployeeFromProject =
|
||||
(projectId: number, userId: string) =>
|
||||
async (dispatch: any, _getState = null, utils: any) => {
|
||||
const response = await handleApiRequest(
|
||||
dispatch,
|
||||
utils.Api.delete(`projects/${projectId}/users/${userId}`),
|
||||
'',
|
||||
REMOVING_EMPLOYEE
|
||||
);
|
||||
|
||||
if (typeof response === 'string') {
|
||||
dispatch(setEmployeeRemoved(true));
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
export { Crew } from './Crew';
|
||||
export { SelectMembersModal } from './SelectMembersModal';
|
||||
export { Members } from './Members';
|
||||
export { Member } from './Member';
|
||||
@@ -0,0 +1,77 @@
|
||||
import {
|
||||
ATTACHING_EMPLOYEES,
|
||||
EMPLOYEE_REMOVED,
|
||||
EMPLOYEES_ATTACHED,
|
||||
FETCHING_CREW_EMPLOYEES,
|
||||
FETCHING_PROJECT_MEMBERS,
|
||||
REMOVING_EMPLOYEE,
|
||||
SET_CREW_EMPLOYEES,
|
||||
SET_PROJECT_MEMBERS,
|
||||
SetCrewTypes,
|
||||
} from 'Containers/Crew/actions';
|
||||
|
||||
const initialState = {
|
||||
employees: [],
|
||||
fetchingCrewEmployees: true,
|
||||
employeesAttached: false,
|
||||
attachingEmployees: false,
|
||||
members: [],
|
||||
fetchingMembers: true,
|
||||
employeeRemoved: undefined,
|
||||
removingEmployee: false,
|
||||
};
|
||||
|
||||
export const crewReducer = (state = initialState, action: SetCrewTypes) => {
|
||||
const { type, payload } = action;
|
||||
|
||||
switch (type) {
|
||||
case SET_CREW_EMPLOYEES: {
|
||||
const { userId, data: employees } = payload;
|
||||
|
||||
return {
|
||||
...state,
|
||||
employees: employees.filter(({ id }: any) => id !== userId), // removing the current user
|
||||
};
|
||||
}
|
||||
case FETCHING_CREW_EMPLOYEES:
|
||||
return {
|
||||
...state,
|
||||
fetchingCrewEmployees: payload,
|
||||
};
|
||||
case EMPLOYEES_ATTACHED:
|
||||
return {
|
||||
...state,
|
||||
employeesAttached: payload,
|
||||
};
|
||||
case ATTACHING_EMPLOYEES:
|
||||
return {
|
||||
...state,
|
||||
attachingEmployees: payload,
|
||||
};
|
||||
case SET_PROJECT_MEMBERS: {
|
||||
const { userId, data: members } = payload;
|
||||
|
||||
return {
|
||||
...state,
|
||||
members: members.filter(({ id }: any) => id !== userId), // removing the current user
|
||||
};
|
||||
}
|
||||
case FETCHING_PROJECT_MEMBERS:
|
||||
return {
|
||||
...state,
|
||||
fetchingMembers: payload,
|
||||
};
|
||||
case EMPLOYEE_REMOVED:
|
||||
return {
|
||||
...state,
|
||||
employeeRemoved: payload,
|
||||
};
|
||||
case REMOVING_EMPLOYEE:
|
||||
return {
|
||||
...state,
|
||||
removingEmployee: payload,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
export const employeesSelector = ({ crew: { employees: value = [] } }: any) => value;
|
||||
export const fetchingCrewEmployeesSelector = ({ crew: { fetchingCrewEmployees: value = true } }: any) => value;
|
||||
export const employeesAttachedSelector = ({ crew: { employeesAttached: value = false } }: any) => value;
|
||||
export const attachingEmployeesSelector = ({ crew: { attachingEmployees: value = false } }: any) => value;
|
||||
export const membersSelector = ({ crew: { members: value = [] } }: any) => value;
|
||||
export const fetchingMembersSelector = ({ crew: { fetchingMembers: value = true } }: any) => value;
|
||||
export const employeeRemovedSelector = ({ crew: { employeeRemoved: value } }: any) => value;
|
||||
export const removingEmployeeSelector = ({ crew: { removingEmployee: value = true } }: any) => value;
|
||||
@@ -0,0 +1,17 @@
|
||||
import React, { memo } from 'react';
|
||||
|
||||
import { areEqual } from 'Utils/equalityChecks';
|
||||
|
||||
import { Dashboard } from 'Components/Dashboard';
|
||||
|
||||
const DashboardContainer = () => {
|
||||
return (
|
||||
<>
|
||||
<Dashboard />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const DashboardContainerMemo = memo(DashboardContainer, areEqual);
|
||||
|
||||
export { DashboardContainerMemo as DashboardContainer };
|
||||
@@ -0,0 +1 @@
|
||||
export { DashboardContainer as Dashboard } from './Dashboard';
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
import React, { memo, useCallback } from 'react';
|
||||
import { areEqual } from 'Utils/equalityChecks';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
|
||||
import { CreateProjectButton } from 'Components/Dashboard';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const CreateProjectButtonContainer = ({ className }: Props) => {
|
||||
const history = useHistory();
|
||||
|
||||
return <CreateProjectButton className={className} />;
|
||||
};
|
||||
|
||||
CreateProjectButtonContainer.defaultProps = {
|
||||
className: null,
|
||||
};
|
||||
|
||||
const CreateProjectButtonContainerMemo = memo(CreateProjectButtonContainer, areEqual);
|
||||
|
||||
export { CreateProjectButtonContainerMemo as CreateProjectButtonContainer };
|
||||
@@ -0,0 +1 @@
|
||||
export { CreateProjectButtonContainer as CreateProjectButton } from "./CreateProjectButton";
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
import React, { memo, ReactNode, useCallback, useEffect, useState } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
import { areEqual } from 'Utils/equalityChecks';
|
||||
import { width } from 'Utils/screen';
|
||||
|
||||
import { DashboardLayout } from 'Components/Layouts';
|
||||
import { IntercomProvider } from 'react-use-intercom';
|
||||
import { MobileWarningModal } from 'Containers/MobileWarningModal';
|
||||
import { HubSpotModal } from 'Containers/HubSpotModal';
|
||||
|
||||
interface Props {
|
||||
children: ReactNode;
|
||||
isFirstTimer?: boolean;
|
||||
}
|
||||
|
||||
const INTERCOM_ID: string = process.env.REACT_INTERCOM_ID;
|
||||
|
||||
const DashboardWrapperContainer = ({ children, isFirstTimer }: Props) => {
|
||||
const location = useLocation();
|
||||
const { pathname } = location;
|
||||
|
||||
// this is to slide the sidebar on desktop view
|
||||
const [sideBarDesktop, setSideBarDesktop] = useState(false);
|
||||
|
||||
// this is to slide the sidebar on mobile view
|
||||
const [sideBarMobile, setSideBarMobile] = useState(false);
|
||||
|
||||
const toggleSideBar = useCallback(() => {
|
||||
if (width <= 991) {
|
||||
setSideBarMobile((prev) => !prev);
|
||||
} else {
|
||||
setSideBarDesktop((prev) => !prev);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// reset the sidebar menu on route change
|
||||
useEffect(() => {
|
||||
setSideBarMobile(false);
|
||||
}, [pathname]);
|
||||
|
||||
// dashboard wrapper is wrapped with intercom provider, so we can access its' features
|
||||
return (
|
||||
<IntercomProvider appId={INTERCOM_ID}>
|
||||
<DashboardLayout
|
||||
toggleSideBar={toggleSideBar}
|
||||
sideBarMobile={sideBarMobile}
|
||||
sideBarDesktop={sideBarDesktop}
|
||||
pathname={pathname}
|
||||
>
|
||||
{children}
|
||||
<HubSpotModal visible={isFirstTimer} />
|
||||
<MobileWarningModal />
|
||||
</DashboardLayout>
|
||||
</IntercomProvider>
|
||||
);
|
||||
};
|
||||
|
||||
DashboardWrapperContainer.defaultProps = {
|
||||
isFirstTimer: false,
|
||||
};
|
||||
|
||||
const DashboardWrapperContainerMemo = memo(DashboardWrapperContainer, areEqual);
|
||||
|
||||
export { DashboardWrapperContainerMemo as DashboardWrapperContainer };
|
||||
@@ -0,0 +1 @@
|
||||
export { DashboardWrapperContainer as DashboardWrapper } from './DashboardWrapper';
|
||||
@@ -0,0 +1,34 @@
|
||||
import React, { memo } from 'react';
|
||||
|
||||
import { areEqual } from 'Utils/equalityChecks';
|
||||
|
||||
import { SideBar } from 'Components/SideBar';
|
||||
import { navItems } from 'Utils/navItems';
|
||||
import { useUser } from 'Context/User';
|
||||
|
||||
interface Props {
|
||||
sideBarDesktop: boolean;
|
||||
sideBarMobile: boolean;
|
||||
toggleSideBar: () => void;
|
||||
pathname: string;
|
||||
}
|
||||
|
||||
const SideBarContainer = ({ sideBarDesktop, sideBarMobile, toggleSideBar, pathname }: Props) => {
|
||||
const user = useUser();
|
||||
|
||||
const { companies } = user;
|
||||
|
||||
return (
|
||||
<SideBar
|
||||
sideBarDesktop={sideBarDesktop}
|
||||
sideBarMobile={sideBarMobile}
|
||||
toggleSideBar={toggleSideBar}
|
||||
navItems={navItems}
|
||||
pathname={pathname}
|
||||
companyName={companies?.[0]?.name}
|
||||
/>
|
||||
);
|
||||
};
|
||||
const SideBarContainerMemo = memo(SideBarContainer, areEqual);
|
||||
|
||||
export { SideBarContainerMemo as SideBarContainer };
|
||||
@@ -0,0 +1 @@
|
||||
export { SideBarContainer as SideBar } from "./SideBar";
|
||||
@@ -0,0 +1,3 @@
|
||||
export { DashboardWrapper } from './DashboardWrapper';
|
||||
export { SideBar } from './SideBar';
|
||||
export { CreateProjectButton } from './CreateProjectButton';
|
||||
@@ -0,0 +1,32 @@
|
||||
export const SET_ACTIVE_PROJECT = 'SET_ACTIVE_PROJECT';
|
||||
export const SET_MOBILE_WARNING_MODAL_SHOWN = 'SET_MOBILE_WARNING_MODAL_SHOWN';
|
||||
|
||||
interface ActionTypes {
|
||||
SET_ACTIVE_PROJECT: string;
|
||||
SET_MOBILE_WARNING_MODAL_SHOWN: boolean;
|
||||
}
|
||||
|
||||
interface MessageAction {
|
||||
type: keyof ActionTypes;
|
||||
payload: string | number | boolean | null;
|
||||
}
|
||||
|
||||
export type setActiveProjectTypes = MessageAction;
|
||||
|
||||
/*
|
||||
* NON-API THUNKS
|
||||
* */
|
||||
|
||||
export const setActiveProject = (value: any) => async (dispatch: any) => {
|
||||
dispatch({
|
||||
type: SET_ACTIVE_PROJECT,
|
||||
payload: value,
|
||||
});
|
||||
};
|
||||
|
||||
export const setMobileWarningModalShown = (state: any) => (dispatch: any) => {
|
||||
dispatch({
|
||||
type: SET_MOBILE_WARNING_MODAL_SHOWN,
|
||||
payload: state,
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,2 @@
|
||||
export { DashboardWrapper, CreateProjectButton, SideBar } from './DashboardWrapper';
|
||||
export { Dashboard } from './Dashboard';
|
||||
@@ -0,0 +1,21 @@
|
||||
import { SET_ACTIVE_PROJECT, setActiveProjectTypes, SET_MOBILE_WARNING_MODAL_SHOWN } from './actions';
|
||||
|
||||
const initialState = {
|
||||
activeProject: null,
|
||||
mobileWarningModalShown: false,
|
||||
};
|
||||
|
||||
export const dashboardReducer = (state = initialState, action: setActiveProjectTypes) => {
|
||||
switch (action.type) {
|
||||
case SET_ACTIVE_PROJECT:
|
||||
return { ...state, activeProject: action.payload };
|
||||
case SET_MOBILE_WARNING_MODAL_SHOWN: {
|
||||
return {
|
||||
...state,
|
||||
mobileWarningModalShown: action.payload,
|
||||
};
|
||||
}
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
export const activeProjectSelector = ({ dashboard: { activeProject: value = false } }: any) => value;
|
||||
|
||||
export const myProjectsSelector = ({ projects: { myProjects: value = {} } }: any) => value;
|
||||
|
||||
export const fetchingMyProjectsSelector = ({ projects: { fetchingMyProjects: value = false } }: any) => value;
|
||||
|
||||
export const mobileWarningModalShownSelector = ({ dashboard: { mobileWarningModalShown: value = false } }: any) =>
|
||||
value;
|
||||
@@ -0,0 +1,57 @@
|
||||
import React, { memo, useCallback, useEffect, useRef } from 'react';
|
||||
import { areEqual } from 'Utils/equalityChecks';
|
||||
import { DropDownItem } from 'Components/DropDown/DropDownItem';
|
||||
import { MiniDropdownItem } from 'Components/DropDown/MiniDropdownItem';
|
||||
import { dropdownSizes } from 'Components/DropDown';
|
||||
|
||||
interface Props {
|
||||
id: number;
|
||||
name: string;
|
||||
selected: number;
|
||||
showDropDown: boolean;
|
||||
size?: string;
|
||||
shouldScroll?: boolean;
|
||||
onSelectItem: (e: any) => void;
|
||||
}
|
||||
|
||||
const DropDownItemContainer = ({ id, name, selected, onSelectItem, showDropDown, size, shouldScroll }: Props) => {
|
||||
const ref = useRef(null);
|
||||
|
||||
const onClick = useCallback((e: any) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (onSelectItem) onSelectItem(id);
|
||||
}, []);
|
||||
|
||||
// pre select 0 on floor numbers
|
||||
// TODO::To be refactor into a separate container
|
||||
const scrollToItem = useCallback(() => {
|
||||
if (typeof selected !== 'undefined') {
|
||||
if (selected === id && showDropDown) {
|
||||
ref.current.scrollIntoView();
|
||||
}
|
||||
}
|
||||
}, [showDropDown]);
|
||||
|
||||
useEffect(() => {
|
||||
if (shouldScroll) {
|
||||
scrollToItem();
|
||||
}
|
||||
}, [showDropDown, shouldScroll]);
|
||||
|
||||
switch (size) {
|
||||
case dropdownSizes.small:
|
||||
return <MiniDropdownItem ref={ref} id={id} name={name} selected={selected} onSelectItem={onClick} />;
|
||||
default:
|
||||
return <DropDownItem ref={ref} id={id} name={name} selected={selected} onSelectItem={onClick} />;
|
||||
}
|
||||
};
|
||||
|
||||
DropDownItemContainer.defaultProps = {
|
||||
size: 'default',
|
||||
shouldScroll: false,
|
||||
};
|
||||
|
||||
const DropDownItemContainerMemo = memo(DropDownItemContainer, areEqual);
|
||||
|
||||
export { DropDownItemContainerMemo as DropDownItem };
|
||||
@@ -0,0 +1 @@
|
||||
export { DropDownItem } from './DropDownItem';
|
||||
@@ -0,0 +1 @@
|
||||
export { DropDownItem } from './DropDownItem';
|
||||
@@ -0,0 +1,115 @@
|
||||
import React, { memo, useCallback } from 'react';
|
||||
|
||||
import { areEqual } from 'Utils/equalityChecks';
|
||||
import { awsStore } from 'Utils/awsStore';
|
||||
import { Dropzone } from 'Components/DropZone';
|
||||
import { useDropzone } from 'react-dropzone';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { handleApiRequest } from 'Utils/handleApiRequest';
|
||||
import { Api } from 'Utils/api';
|
||||
|
||||
import { showToast } from 'Containers/PhotoErrorToast/actions';
|
||||
import { attachAlbumPhoto, setFileUploadingRoom } from 'Containers/DropZone/actions';
|
||||
|
||||
interface Props {
|
||||
roomId: number;
|
||||
albumId?: number;
|
||||
hasImages: boolean;
|
||||
setImageList?: (imgPath: object) => void;
|
||||
}
|
||||
|
||||
// This constructs the urls to view in the PhotoViewer
|
||||
const imageData = (id: number, raw: string | ArrayBuffer, large: string | ArrayBuffer) => ({
|
||||
id,
|
||||
thumbnailSrcUrl: large,
|
||||
srcUrl: raw,
|
||||
});
|
||||
|
||||
const postTransformObject = (projectId: number | string, contentType: string, response: any) =>
|
||||
// Create a new object to match the json schema for the back end request
|
||||
({
|
||||
uuid: response.uuid,
|
||||
s3_key: response.key,
|
||||
bucket: response.bucket,
|
||||
file_name: `${response.name}${response.extension}`,
|
||||
file_extension: response.extension,
|
||||
content_type: contentType,
|
||||
project_id: projectId,
|
||||
});
|
||||
|
||||
const projectIdSelector = ({ projects: { selectedProjectId } }) => selectedProjectId;
|
||||
|
||||
const DropzoneContainer = ({ roomId, albumId, hasImages, setImageList }: Props) => {
|
||||
const projectId = useSelector(projectIdSelector, areEqual);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const onDrop = useCallback(async (acceptedFiles: any[], rejectedFiles: any[]) => {
|
||||
if (rejectedFiles.length > 0) {
|
||||
dispatch(showToast());
|
||||
}
|
||||
|
||||
// this is to show the spinner on specific room and album
|
||||
dispatch(setFileUploadingRoom({ roomId, albumId }));
|
||||
const numberOfFiles = acceptedFiles.length;
|
||||
|
||||
acceptedFiles.forEach(async (file: any, index: number) => {
|
||||
const reader = new FileReader();
|
||||
|
||||
// For future reference
|
||||
// reader.onabort = () => {
|
||||
// //send a message to the Error Messages component
|
||||
// };
|
||||
|
||||
// For future reference
|
||||
// reader.onerror = () => {
|
||||
// //send a message to the Error Messages component
|
||||
// };
|
||||
|
||||
reader.onload = async () => {
|
||||
// Do whatever you want with the file contents
|
||||
const binaryStr = reader.result;
|
||||
// setImageList(imageData(null, binaryStr, binaryStr));
|
||||
const response = await awsStore(file, binaryStr);
|
||||
|
||||
// Create a new object to match the back end structure
|
||||
const transformed = postTransformObject(projectId, file.type, response);
|
||||
const apiResponse = await handleApiRequest(dispatch, Api.post(`/rooms/${roomId}/photos`, transformed));
|
||||
|
||||
if (apiResponse?.data) {
|
||||
const {
|
||||
data: {
|
||||
id,
|
||||
sizes: { raw, large },
|
||||
},
|
||||
} = apiResponse;
|
||||
|
||||
// this will attach each photo to a album, and also give the signal to refresh the specific gallery
|
||||
dispatch(attachAlbumPhoto(id, albumId, { roomId, albumId, refresh: index === numberOfFiles - 1 }));
|
||||
|
||||
// will be used for story
|
||||
if (setImageList) {
|
||||
setImageList(imageData(id, raw, large));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
}, []);
|
||||
const { getRootProps, getInputProps } = useDropzone({ onDrop, accept: 'image/jpeg, image/png' });
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dropzone hasImages={hasImages} getRootProps={getRootProps} getInputProps={getInputProps} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
DropzoneContainer.defaultProps = {
|
||||
albumId: undefined,
|
||||
setImageList: undefined,
|
||||
};
|
||||
|
||||
const DropzoneContainerMemo = memo(DropzoneContainer, areEqual);
|
||||
|
||||
export { DropzoneContainerMemo as Dropzone };
|
||||
@@ -0,0 +1,54 @@
|
||||
/* eslint-disable */
|
||||
|
||||
import { handleApiRequest } from 'Utils/handleApiRequest';
|
||||
|
||||
export const FILE_UPLOADING_ROOM_ALBUM = 'FILE_UPLOADING_ROOM_ALBUM';
|
||||
export const REFRESH_ROOM_PHOTOS = 'REFRESH_ROOM_PHOTOS';
|
||||
|
||||
interface ActionTypes {
|
||||
FILE_UPLOADING_ROOM_ALBUM: number;
|
||||
REFRESH_ROOM_PHOTOS: any;
|
||||
}
|
||||
|
||||
interface MessageAction {
|
||||
type: keyof ActionTypes;
|
||||
payload: any;
|
||||
}
|
||||
|
||||
export type setDropZoneActionTypes = MessageAction;
|
||||
|
||||
export const attachAlbumPhoto =
|
||||
(photoId: number, albumId: number, refreshPhotos?: any) =>
|
||||
async (dispatch: any, _getState = null, utils: any) => {
|
||||
const response = await handleApiRequest(
|
||||
dispatch,
|
||||
utils.Api.post(
|
||||
`photos/${photoId}/albums/${albumId}
|
||||
`,
|
||||
{}
|
||||
)
|
||||
);
|
||||
|
||||
// refresh photo gallery photos once the last item attached to the album
|
||||
if (typeof response === 'string' && refreshPhotos?.refresh) {
|
||||
dispatch(setRefreshRoomPhotos(refreshPhotos));
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
* NON API THUNKS
|
||||
* */
|
||||
|
||||
export const setFileUploadingRoom = (roomAndAlbum: any) => async (dispatch: any) => {
|
||||
dispatch({
|
||||
type: FILE_UPLOADING_ROOM_ALBUM,
|
||||
payload: roomAndAlbum,
|
||||
});
|
||||
};
|
||||
|
||||
export const setRefreshRoomPhotos = (value: any) => async (dispatch: any) => {
|
||||
dispatch({
|
||||
type: REFRESH_ROOM_PHOTOS,
|
||||
payload: value,
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { Dropzone } from "./DropZone";
|
||||
@@ -0,0 +1,25 @@
|
||||
import { FILE_UPLOADING_ROOM_ALBUM, REFRESH_ROOM_PHOTOS, setDropZoneActionTypes } from 'Containers/DropZone/actions';
|
||||
|
||||
const initialState = {
|
||||
fileUploadingRoomAndRoom: undefined,
|
||||
refreshRoomPhotos: {},
|
||||
};
|
||||
|
||||
export const dropZoneReducer = (state = initialState, action: setDropZoneActionTypes) => {
|
||||
const { type, payload } = action;
|
||||
|
||||
switch (type) {
|
||||
case FILE_UPLOADING_ROOM_ALBUM:
|
||||
return {
|
||||
...state,
|
||||
fileUploadingRoomAndRoom: payload,
|
||||
};
|
||||
case REFRESH_ROOM_PHOTOS:
|
||||
return {
|
||||
...state,
|
||||
refreshRoomPhotos: payload,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,2 @@
|
||||
export const fileUploadingRoomAndRoomSelector = ({ dropzone: { fileUploadingRoomAndRoom: value } }) => value;
|
||||
export const refreshRoomPhotosSelector = ({ dropzone: { refreshRoomPhotos: value } }) => value;
|
||||
@@ -0,0 +1,88 @@
|
||||
import React, { memo, useCallback, useEffect, useState } from 'react';
|
||||
import { areEqual } from 'Utils/equalityChecks';
|
||||
import { FloorDropDown } from 'Components/FloorDropDown';
|
||||
|
||||
import { floorNumbers } from 'Utils/helpers';
|
||||
|
||||
interface Props {
|
||||
floorNumber: number;
|
||||
invalid?: boolean;
|
||||
hideDropDown: boolean;
|
||||
setFloorNumber?: (e: any) => void;
|
||||
}
|
||||
|
||||
const FloorDropDownContainer = ({ floorNumber, invalid, hideDropDown, setFloorNumber }: Props) => {
|
||||
const [selectedValue, setSelectedValue]: any = useState('0');
|
||||
const [numbers, setNumbers] = useState(floorNumbers());
|
||||
const [showDropDown, setShowDropDown] = useState(false);
|
||||
|
||||
const floorList = floorNumbers();
|
||||
|
||||
useEffect(() => {
|
||||
if (floorNumber.toString() !== '0') {
|
||||
setNumbers([]);
|
||||
setSelectedValue(floorNumber);
|
||||
setNumbers(floorNumbers());
|
||||
}
|
||||
}, [floorNumber]);
|
||||
|
||||
useEffect(() => {
|
||||
if (hideDropDown) {
|
||||
setShowDropDown(false);
|
||||
}
|
||||
}, [hideDropDown]);
|
||||
|
||||
const onChangeFloorSelect = useCallback(
|
||||
(e: any) => {
|
||||
const { value } = e.target;
|
||||
|
||||
if (value.length > 0 && floorList.length > 0) {
|
||||
setShowDropDown(true);
|
||||
setNumbers(floorNumbers().filter((floor: any) => floor.name.includes(value)));
|
||||
} else {
|
||||
setNumbers(floorList);
|
||||
}
|
||||
setSelectedValue(value);
|
||||
},
|
||||
[numbers]
|
||||
);
|
||||
|
||||
const onSelectItem = useCallback(
|
||||
(id: number) => {
|
||||
setFloorNumber(id);
|
||||
if (numbers.length > 0) {
|
||||
const floor = numbers.find((floor: any) => floor.id === id);
|
||||
setSelectedValue(floor.name);
|
||||
}
|
||||
|
||||
setShowDropDown(false);
|
||||
},
|
||||
[numbers]
|
||||
);
|
||||
|
||||
const onClickIcon = useCallback(() => {
|
||||
setShowDropDown((prevState) => !prevState);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<FloorDropDown
|
||||
floorSelect={selectedValue}
|
||||
selectedFloor={floorNumber}
|
||||
numbers={numbers}
|
||||
showDropDown={showDropDown}
|
||||
invalid={invalid}
|
||||
onChangeFloorSelect={onChangeFloorSelect}
|
||||
onSelectItem={onSelectItem}
|
||||
onClickIcon={onClickIcon}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
FloorDropDownContainer.defaultProps = {
|
||||
invalid: false,
|
||||
setFloorNumber: undefined,
|
||||
};
|
||||
|
||||
const FloorDropDownContainerMemo = memo(FloorDropDownContainer, areEqual);
|
||||
|
||||
export { FloorDropDownContainerMemo as FloorDropDown };
|
||||
@@ -0,0 +1 @@
|
||||
export { FloorDropDown } from './FloorDropDown';
|
||||
@@ -0,0 +1,39 @@
|
||||
import React, { memo, ReactNode } from 'react';
|
||||
|
||||
import { areEqualShallow } from 'Utils/equalityChecks';
|
||||
import { Form } from 'Components/Form';
|
||||
|
||||
interface Props {
|
||||
id?: string;
|
||||
className?: string;
|
||||
noValidate?: boolean;
|
||||
submitButton?: ReactNode;
|
||||
children: any;
|
||||
onSubmit: (formValues: any) => void;
|
||||
}
|
||||
|
||||
/*
|
||||
Note:
|
||||
The form can use either a Submit button or the internal onChange to send form data back to the user
|
||||
|
||||
A form button needs to be pass in through props and not children. We need a way to detect if a button
|
||||
is being used or not to handle the form submission
|
||||
*/
|
||||
const FormContainer = ({ id, className, noValidate = true, submitButton, children, onSubmit }: Props) => (
|
||||
// Using a reference is how we are going to do things the React way, with Bootstrap.
|
||||
|
||||
<Form id={id} className={className} noValidate={noValidate} onSubmit={onSubmit}>
|
||||
{children}
|
||||
{submitButton && <div className="col-12">{submitButton}</div>}
|
||||
</Form>
|
||||
);
|
||||
FormContainer.defaultProps = {
|
||||
id: undefined,
|
||||
className: undefined,
|
||||
noValidate: true,
|
||||
submitButton: undefined,
|
||||
};
|
||||
|
||||
const FormContainerMemo = memo(FormContainer, areEqualShallow);
|
||||
|
||||
export { FormContainerMemo as Form };
|
||||
@@ -0,0 +1 @@
|
||||
export { Form } from './Form';
|
||||
@@ -0,0 +1,60 @@
|
||||
import React, { memo, useCallback } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { Icon } from 'Components/Icons';
|
||||
import { LevelSelector } from 'Containers/RocketScan/RoomsView/RoomContent/LevelSelector/LevelSelector';
|
||||
import { updateRoomLevel } from 'Containers/Project/Unit/Rooms/actions';
|
||||
|
||||
import { areEqual } from 'Utils/equalityChecks';
|
||||
|
||||
import classes from './galleryHeader.module.css';
|
||||
|
||||
interface Props {
|
||||
roomName: string;
|
||||
roomId: number;
|
||||
levelName?: string;
|
||||
}
|
||||
|
||||
// temporary user feedback during development
|
||||
// const temporaryButtonAction = () => alert("Temporarily disabled for development");
|
||||
|
||||
const GalleryHeaderContainer = ({ roomName, roomId, levelName }: Props) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const onLevelChange = useCallback((e: any) => {
|
||||
e.preventDefault();
|
||||
const { id } = e.currentTarget;
|
||||
|
||||
// dispatch update room
|
||||
dispatch(updateRoomLevel(roomId, id));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={`container-fluid d-flex flex-row justify-content-start px-0 py-2 ${classes.headerWrapper}`}>
|
||||
<div className="col d-flex flex-row justify-content-start align-items-baseline">
|
||||
<div className={classes.imageWrapper}>
|
||||
<Icon type="kitchen" />
|
||||
</div>
|
||||
<h2 className={classes.roomName}>{roomName}</h2>
|
||||
</div>
|
||||
<div className="col d-flex flex-row justify-content-end align-items-center position-relative">
|
||||
<LevelSelector
|
||||
defaultSelectedItem={levelName}
|
||||
defaultAccordionCollapse
|
||||
accordionId={`${roomId}-accordion`}
|
||||
levelMenuHeading={`${roomId}-heading`}
|
||||
levelOptions={`${roomId}-options`}
|
||||
onLevelChange={onLevelChange}
|
||||
/>
|
||||
{/* <Icon type="actionsdefault" onClick={temporaryButtonAction} /> */}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
GalleryHeaderContainer.defaultProps = {
|
||||
levelName: 'Main Level',
|
||||
};
|
||||
|
||||
const GalleryHeaderContainerMemo = memo(GalleryHeaderContainer, areEqual);
|
||||
|
||||
export { GalleryHeaderContainerMemo as GalleryHeader };
|
||||
@@ -0,0 +1,26 @@
|
||||
.headerWrapper {
|
||||
border-bottom: 1px solid rgba(154, 0, 255, 0.5);
|
||||
}
|
||||
|
||||
.imageWrapper {
|
||||
transform: scale(0.55);
|
||||
transform-origin: left;
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
}
|
||||
|
||||
.roomName {
|
||||
font-family: IBM Plex Sans;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
font-size: 20px;
|
||||
line-height: 30px;
|
||||
color: #6d00e6;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.menuWrapper {
|
||||
padding: 3px 1px 5px;
|
||||
background-color: #e8e7ed;
|
||||
border-radius: 25px;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { GalleryHeader } from "./GalleryHeader";
|
||||
@@ -0,0 +1,46 @@
|
||||
import React, { memo, useEffect, useState } from 'react';
|
||||
import { areEqual } from 'Utils/equalityChecks';
|
||||
import { HubSpotModal } from 'Components/HubSpotModal';
|
||||
|
||||
const HubSpotModalContainer = ({ visible }: { visible: boolean }) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [chosenAction, setChosenAction] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
const script = document.createElement('script');
|
||||
script.src = 'https://static.hsappstatic.net/MeetingsEmbed/ex/MeetingsEmbedCode.js';
|
||||
document.body.appendChild(script);
|
||||
|
||||
script.addEventListener('load', () => {
|
||||
setIsOpen(true);
|
||||
});
|
||||
}
|
||||
return () => isOpen && setIsOpen(false);
|
||||
}, [visible]);
|
||||
|
||||
const modalCloseClick = React.useCallback(() => setIsOpen(false), []);
|
||||
const chooseBook = React.useCallback(() => setChosenAction(1), []);
|
||||
const chooseSkip = React.useCallback(() => setChosenAction(0), []);
|
||||
const closeAll = React.useCallback(() => {
|
||||
setChosenAction(-1);
|
||||
setIsOpen(false);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
visible && (
|
||||
<HubSpotModal
|
||||
closeAll={closeAll}
|
||||
chosenAction={chosenAction}
|
||||
chooseBook={chooseBook}
|
||||
chooseSkip={chooseSkip}
|
||||
isOpen={isOpen}
|
||||
modalCloseClick={modalCloseClick}
|
||||
/>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const HubSpotModalContainerMemo = memo(HubSpotModalContainer, areEqual);
|
||||
|
||||
export { HubSpotModalContainerMemo as HubSpotModalContainer };
|
||||
@@ -0,0 +1 @@
|
||||
export { HubSpotModalContainer as HubSpotModal } from './HubSpotModal';
|
||||
@@ -0,0 +1,34 @@
|
||||
import React, { memo, useCallback } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import { areEqual } from 'Utils/equalityChecks';
|
||||
import { ImageDeleteModal } from 'Components/ImageDeleteModal';
|
||||
|
||||
import { deletePhoto } from 'Containers/Thumbnail/actions';
|
||||
|
||||
interface Props {
|
||||
id: number;
|
||||
isOpen: boolean;
|
||||
modalCloseClick: (e: any) => void;
|
||||
}
|
||||
|
||||
const ImageDeleteModalContainer = ({ id, isOpen, modalCloseClick }: Props) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const onDeleteButtonClick = useCallback(() => {
|
||||
dispatch(deletePhoto(id));
|
||||
}, [id]);
|
||||
|
||||
return (
|
||||
<ImageDeleteModal
|
||||
id={id}
|
||||
isOpen={isOpen}
|
||||
modalCloseClick={modalCloseClick}
|
||||
onDeleteButtonClick={onDeleteButtonClick}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const ImageDeleteModalContainerMemo = memo(ImageDeleteModalContainer, areEqual);
|
||||
|
||||
export { ImageDeleteModalContainerMemo as ImageDeleteModal };
|
||||
@@ -0,0 +1 @@
|
||||
export { ImageDeleteModal } from "./ImageDeleteModal";
|
||||
@@ -0,0 +1,30 @@
|
||||
import React, { memo, ReactNode } from 'react';
|
||||
|
||||
import { areEqual } from 'Utils/equalityChecks';
|
||||
|
||||
import classes from './imageTile.module.css';
|
||||
|
||||
interface Props {
|
||||
caption?: string;
|
||||
icon?: ReactNode;
|
||||
sizeSmall?: boolean;
|
||||
onTileClick?: (e: any) => void;
|
||||
}
|
||||
|
||||
const ImageTileContainer = ({ caption, icon, sizeSmall, onTileClick }: Props) => (
|
||||
<button className={`${classes.imageTileCard} ${sizeSmall ? classes.sizeSmall : ''}`} onClick={onTileClick}>
|
||||
<div className={classes.iconImage}>{icon}</div>
|
||||
<h2 className={classes.imageCaption}>{caption}</h2>
|
||||
</button>
|
||||
);
|
||||
|
||||
ImageTileContainer.defaultProps = {
|
||||
caption: undefined,
|
||||
icon: undefined,
|
||||
sizeSmall: false,
|
||||
onTileClick: undefined,
|
||||
};
|
||||
|
||||
const ImageTileContainerMemo = memo(ImageTileContainer, areEqual);
|
||||
|
||||
export { ImageTileContainerMemo as ImageTile };
|
||||
@@ -0,0 +1,42 @@
|
||||
.imageTileCard {
|
||||
height: 199px;
|
||||
width: 199px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border: 1px solid #d2cfda;
|
||||
border-radius: 10px;
|
||||
margin: 0 0.75em;
|
||||
background-color: unset !important;
|
||||
}
|
||||
|
||||
.imageTileCard:hover {
|
||||
box-shadow: 0px 6px 12px rgba(142, 134, 163, 0.2);
|
||||
border-color: rgba(154, 0, 255, 0.5);
|
||||
transition: ease-in-out 0.2s;
|
||||
}
|
||||
|
||||
.iconImage {
|
||||
}
|
||||
|
||||
.imageCaption {
|
||||
font-family: IBM Plex Sans;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
font-size: 20px;
|
||||
line-height: 31px;
|
||||
text-align: center;
|
||||
}
|
||||
.sizeSmall {
|
||||
width: 156px;
|
||||
height: 156px;
|
||||
padding-top: 36px;
|
||||
margin: 0;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.sizeSmall .imageCaption {
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { ImageTile } from './ImageTile';
|
||||
@@ -0,0 +1,134 @@
|
||||
import React, { memo, useCallback, useState, useEffect } from 'react';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
|
||||
import { areEqual } from 'Utils/equalityChecks';
|
||||
import { InviteEmployees } from 'Components/InviteEmployees';
|
||||
import { firstCompanyIdSelector } from 'Containers/Projects/selectors';
|
||||
import { sendInviteSelector, emailInviteLinkSelector, emailErrorSelector } from './selectors';
|
||||
import { sendInviteLink, setInviteSent, getInviteURL } from './actions';
|
||||
|
||||
interface Props {
|
||||
isOpen?: boolean;
|
||||
modalCloseClick: (e: any) => void;
|
||||
header?: any;
|
||||
footer?: any;
|
||||
}
|
||||
|
||||
const InviteEmployeesContainer = ({ isOpen, modalCloseClick, header, footer }: Props) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const sent = useSelector(sendInviteSelector, areEqual);
|
||||
const firstCompanyId = useSelector(firstCompanyIdSelector, areEqual);
|
||||
const emailLink = useSelector(emailInviteLinkSelector, areEqual);
|
||||
|
||||
const [isButtonDisabled, setIsButtonDisabled] = useState(true);
|
||||
const [email, setEmail] = useState('');
|
||||
const [toastMessage, setToastMessage] = useState('');
|
||||
const [showToast, setShowToast] = useState(false);
|
||||
const [tempEmail, setTempEmail] = useState('');
|
||||
|
||||
// email field is only cleared when email is sent and the modal is closed
|
||||
const [canClearEmail, setCanClearEmail] = useState(false);
|
||||
|
||||
// API errors
|
||||
const errors = {
|
||||
email: useSelector(emailErrorSelector, areEqual),
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(getInviteURL(firstCompanyId));
|
||||
}, []);
|
||||
|
||||
useEffect(
|
||||
() => () => {
|
||||
if (sent) {
|
||||
setCanClearEmail(true);
|
||||
dispatch(setInviteSent(false));
|
||||
}
|
||||
},
|
||||
[sent]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen && canClearEmail) {
|
||||
setTempEmail('');
|
||||
setCanClearEmail(false);
|
||||
}
|
||||
}, [isOpen, canClearEmail]);
|
||||
|
||||
const onCopyClick = useCallback((e: any) => {
|
||||
e.preventDefault();
|
||||
setToastMessage('Link Copied');
|
||||
setShowToast(true);
|
||||
}, []);
|
||||
|
||||
const onEmailChange = useCallback((e: any) => {
|
||||
const { value } = e.target;
|
||||
setIsButtonDisabled(value.length === 0);
|
||||
setTempEmail(value);
|
||||
}, []);
|
||||
|
||||
const onFormSubmit = useCallback((formData: any) => {
|
||||
const { email } = formData;
|
||||
setEmail(email);
|
||||
}, []);
|
||||
|
||||
const onSendClick = useCallback(
|
||||
(e: any) => {
|
||||
e.preventDefault();
|
||||
dispatch(sendInviteLink(firstCompanyId, email));
|
||||
setToastMessage('Link Sent');
|
||||
},
|
||||
[email]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (sent) {
|
||||
setTimeout(() => {
|
||||
dispatch(setInviteSent(false));
|
||||
}, 1500);
|
||||
setShowToast(true);
|
||||
}
|
||||
}, [sent]);
|
||||
|
||||
// Toast timeout
|
||||
useEffect(() => {
|
||||
if (showToast) {
|
||||
setTimeout(() => {
|
||||
setShowToast(false);
|
||||
}, 1500);
|
||||
}
|
||||
}, [showToast]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<InviteEmployees
|
||||
header={header}
|
||||
footer={footer}
|
||||
emailLink={emailLink}
|
||||
inviteEmail={tempEmail}
|
||||
onEmailChange={onEmailChange}
|
||||
isButtonDisabled={isButtonDisabled}
|
||||
onCopyClick={onCopyClick}
|
||||
onFormSubmit={onFormSubmit}
|
||||
isOpen={isOpen}
|
||||
onSendClick={onSendClick}
|
||||
formErrors={errors}
|
||||
showToast={showToast}
|
||||
toastMessage={toastMessage}
|
||||
onClickCloseInviteEmployees={modalCloseClick}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
InviteEmployeesContainer.defaultProps = {
|
||||
isOpen: false,
|
||||
header: null,
|
||||
footer: null,
|
||||
};
|
||||
|
||||
// This allows for default props if they exist
|
||||
const InviteEmployeesContainerMemo = memo(InviteEmployeesContainer, areEqual);
|
||||
|
||||
export { InviteEmployeesContainerMemo as InviteEmployees };
|
||||
@@ -0,0 +1,57 @@
|
||||
/* eslint-disable */
|
||||
import { handleApiRequest } from 'Utils/handleApiRequest';
|
||||
|
||||
export const SET_INVITE_SENT = 'SET_INVITE_SENT';
|
||||
export const GET_EMAIL_INVITE = 'GET_EMAIL_INVITE';
|
||||
|
||||
interface ActionTypes {
|
||||
SET_INVITE_SENT: object;
|
||||
GET_EMAIL_INVITE: object;
|
||||
}
|
||||
|
||||
interface Payload {
|
||||
email: string;
|
||||
}
|
||||
|
||||
export interface MessageAction {
|
||||
type: keyof ActionTypes;
|
||||
payload: Payload | undefined;
|
||||
}
|
||||
|
||||
export type sendInviteActionTypes = MessageAction;
|
||||
|
||||
export const sendInviteLink =
|
||||
(companyId: string, email: string) =>
|
||||
async (dispatch: any, _getState = null, utils: any) => {
|
||||
const response = await handleApiRequest(
|
||||
dispatch,
|
||||
utils.Api.post(`/dispatch-company-invite/${companyId}`, { email })
|
||||
);
|
||||
|
||||
if (typeof response === 'string') {
|
||||
dispatch(setInviteSent(true));
|
||||
}
|
||||
};
|
||||
|
||||
export const setInviteSent = (status: boolean) => async (dispatch: any) => {
|
||||
dispatch({
|
||||
type: SET_INVITE_SENT,
|
||||
payload: status,
|
||||
});
|
||||
};
|
||||
|
||||
export const getInviteURL =
|
||||
(companyId: string) =>
|
||||
async (dispatch: any, _getState = null, utils: any) => {
|
||||
const response = await handleApiRequest(dispatch, utils.Api.get(`/show-company-invite-urls/${companyId}`));
|
||||
|
||||
if (response?.data) {
|
||||
const { data } = response;
|
||||
const emailInviteLink = data?.shortened_deep_link?.javascript_redirect;
|
||||
|
||||
dispatch({
|
||||
type: GET_EMAIL_INVITE,
|
||||
payload: emailInviteLink,
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { InviteEmployees } from './InviteEmployees';
|
||||
@@ -0,0 +1,24 @@
|
||||
import { SET_INVITE_SENT, GET_EMAIL_INVITE, sendInviteActionTypes } from './actions';
|
||||
|
||||
const initialState = {
|
||||
sent: false,
|
||||
emailInviteLink: undefined,
|
||||
};
|
||||
|
||||
export const employeeInviteReducer = (state = initialState, action: sendInviteActionTypes) => {
|
||||
const { type, payload } = action;
|
||||
switch (type) {
|
||||
case SET_INVITE_SENT:
|
||||
return {
|
||||
...state,
|
||||
sent: payload,
|
||||
};
|
||||
case GET_EMAIL_INVITE:
|
||||
return {
|
||||
...state,
|
||||
emailInviteLink: payload,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
export const sendInviteSelector = ({ employeeInvite: { sent: value = false } }) => value;
|
||||
export const emailInviteLinkSelector = ({ employeeInvite: { emailInviteLink: value = '' } }) => value;
|
||||
|
||||
export const emailErrorSelector = ({ core: { formErrors } }: any) => formErrors?.email || [];
|
||||
@@ -0,0 +1,38 @@
|
||||
import React, { memo, ReactNode, useCallback } from 'react';
|
||||
import { areEqual } from 'Utils/equalityChecks';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
|
||||
import { useSelector } from 'react-redux';
|
||||
import { projectIdSelector, projectUnavailableSelector } from 'Containers/RocketScan/selectors';
|
||||
|
||||
import { ProjectTabsLayout } from 'Components/Layouts/ProjectTabsLayout';
|
||||
|
||||
interface Props {
|
||||
tab: string;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
const ProjectTabsLayoutContainer = ({ children, tab }: Props) => {
|
||||
const history = useHistory();
|
||||
|
||||
const projectId = useSelector(projectIdSelector);
|
||||
const projectUnavailable = useSelector(projectUnavailableSelector);
|
||||
|
||||
// redirect to the specific tab route
|
||||
const onTabClick = useCallback(
|
||||
(tab: string) => {
|
||||
history.push(`/projects/${projectId}/${tab}`);
|
||||
},
|
||||
[projectId]
|
||||
);
|
||||
|
||||
return (
|
||||
<ProjectTabsLayout tab={tab} projectUnavailable={projectUnavailable} onTabClick={onTabClick}>
|
||||
{children}
|
||||
</ProjectTabsLayout>
|
||||
);
|
||||
};
|
||||
|
||||
const ProjectTabsLayoutContainerMemo = memo(ProjectTabsLayoutContainer, areEqual);
|
||||
|
||||
export { ProjectTabsLayoutContainerMemo as ProjectTabsLayout };
|
||||
@@ -0,0 +1 @@
|
||||
export { ProjectTabsLayout } from './ProjectTabsLayout';
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user