Skip to content
This repository was archived by the owner on Mar 13, 2025. It is now read-only.

Final fixes and new features #21

Merged
merged 9 commits into from
Jun 14, 2021
7 changes: 6 additions & 1 deletion src/components/Page/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React from "react";
import PT from "prop-types";
import cn from "classnames";
import ReduxToastr from "react-redux-toastr";
import { TOAST_DEFAULT_TIMEOUT } from "constants/index.js";
import styles from "./styles.module.scss";

/**
Expand All @@ -16,8 +17,12 @@ const Page = ({ className, children }) => (
<div className={cn(styles.container, className)}>
{children}
<ReduxToastr
timeOut={60000}
timeOut={TOAST_DEFAULT_TIMEOUT}
position="top-right"
newestOnTop={true}
removeOnHover={false}
removeOnHoverTimeOut={TOAST_DEFAULT_TIMEOUT}
closeOnToastrClick={false}
transitionIn="fadeIn"
transitionOut="fadeOut"
/>
Expand Down
25 changes: 25 additions & 0 deletions src/components/ProjectName/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import React, { useContext, useEffect } from "react";
import PT from "prop-types";
import cn from "classnames";
import { ProjectNameContext } from "components/ProjectNameContextProvider";
import styles from "./styles.module.scss";

const ProjectName = ({ className, projectId }) => {
const [getName, fetchName] = useContext(ProjectNameContext);
useEffect(() => {
fetchName(projectId);
}, [fetchName, projectId]);

return (
<span className={cn(styles.container, className)}>
{getName(projectId) || projectId}
</span>
);
};

ProjectName.propTypes = {
className: PT.string,
projectId: PT.number.isRequired,
};

export default ProjectName;
5 changes: 5 additions & 0 deletions src/components/ProjectName/styles.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
@import "styles/mixins";

.container {
@include roboto-medium;
}
47 changes: 47 additions & 0 deletions src/components/ProjectNameContextProvider/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import React, { createContext, useCallback, useState } from "react";
import PT from "prop-types";
import { fetchProject } from "services/workPeriods";
import { increment, noop } from "utils/misc";

const names = {};
const promises = {};

const getName = (id) => names[id];

export const ProjectNameContext = createContext([
getName,
(id) => {
`${id}`;
},
]);

const ProjectNameProvider = ({ children }) => {
const [, setCount] = useState(Number.MIN_SAFE_INTEGER);

const fetchName = useCallback((id) => {
if (id in names || id in promises) {
return;
}
promises[id] = fetchProject(id)
.then((data) => {
names[id] = data.name;
setCount(increment);
})
.catch(noop)
.finally(() => {
delete promises[id];
});
}, []);

return (
<ProjectNameContext.Provider value={[getName, fetchName]}>
{children}
</ProjectNameContext.Provider>
);
};

ProjectNameProvider.propTypes = {
children: PT.node,
};

export default ProjectNameProvider;
89 changes: 51 additions & 38 deletions src/components/SearchHandleField/index.jsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
import React, { useCallback, useState } from "react";
import React, { useCallback } from "react";
import PT from "prop-types";
import cn from "classnames";
import _ from "lodash";
import AsyncSelect from "react-select/async";
import { getMemberSuggestions } from "services/teams";
// import { getOptionByValue } from "utils/misc";
import styles from "./styles.module.scss";

const selectComponents = {
DropdownIndicator: () => null,
IndicatorSeparator: () => null,
};

const loadingMessage = () => "Loading...";

const noOptionsMessage = () => "No suggestions";

/**
* Displays search input field.
*
Expand All @@ -25,20 +27,31 @@ const selectComponents = {
* @param {string} props.value input value
* @returns {JSX.Element}
*/
const SearchAutocomplete = ({
const SearchHandleField = ({
className,
id,
name,
size = "medium",
onChange,
placeholder,
value,
}) => {
// const option = getOptionByValue(options, value);
const [savedInput, setSavedInput] = useState("");

const onValueChange = useCallback(
(option) => {
onChange(option.value);
(option, { action }) => {
if (action === "clear") {
onChange("");
} else {
onChange(option.value);
}
},
[onChange]
);

const onInputChange = useCallback(
(value, { action }) => {
if (action === "input-change") {
onChange(value);
}
},
[onChange]
);
Expand All @@ -51,52 +64,52 @@ const SearchAutocomplete = ({
classNamePrefix="custom"
components={selectComponents}
id={id}
name={name}
isClearable={true}
isSearchable={true}
// menuIsOpen={true} // for debugging
// onChange={onOptionChange}
// onMenuOpen={onMenuOpen}
// onMenuClose={onMenuClose}
value={{ value, label: value }}
onInputChange={setSavedInput}
onFocus={() => {
setSavedInput("");
onChange(savedInput);
}}
placeholder={placeholder}
value={null}
inputValue={value}
onChange={onValueChange}
noOptionsMessage={() => "No options"}
loadingMessage={() => "Loading..."}
onInputChange={onInputChange}
openMenuOnClick={false}
placeholder={placeholder}
noOptionsMessage={noOptionsMessage}
loadingMessage={loadingMessage}
loadOptions={loadSuggestions}
blurInputOnSelect
cacheOptions
/>
</div>
);
};

const loadSuggestions = (inputVal) => {
return getMemberSuggestions(inputVal)
.then((res) => {
const users = _.get(res, "data.result.content", []);
return users.map((user) => ({
label: user.handle,
value: user.handle,
}));
})
.catch(() => {
console.warn("could not get suggestions");
return [];
});
const loadSuggestions = async (inputVal) => {
let options = [];
if (inputVal.length < 3) {
return options;
}
try {
const res = await getMemberSuggestions(inputVal);
const users = res.data.result.content;
for (let i = 0, len = users.length; i < len; i++) {
let value = users[i].handle;
options.push({ value, label: value });
}
} catch (error) {
console.error(error);
console.warn("could not get suggestions");
}
return options;
};

SearchAutocomplete.propTypes = {
SearchHandleField.propTypes = {
className: PT.string,
id: PT.string.isRequired,
size: PT.oneOf(["medium", "small"]),
name: PT.string.isRequired,
onChange: PT.func.isRequired,
options: PT.array,
placeholder: PT.string,
value: PT.oneOfType([PT.number, PT.string]),
};

export default SearchAutocomplete;
export default SearchHandleField;
25 changes: 20 additions & 5 deletions src/components/SearchHandleField/styles.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
@import "styles/mixins";

.container {
position: relative;
display: flex;
align-items: center;
border: 1px solid $control-border-color;
Expand All @@ -18,6 +19,11 @@
}

.icon {
display: block;
position: absolute;
top: 0;
bottom: 0;
left: 0;
margin: auto 10px;
width: 16px;
height: 16px;
Expand All @@ -42,7 +48,7 @@ input.input {
display: flex;
margin: 0;
border: none !important;
padding: 8px 16px 8px 0;
padding: 8px 16px 8px 36px;
line-height: 22px;
background: none;
outline: none !important;
Expand All @@ -68,6 +74,7 @@ input.input {
margin: 0;
border: none;
padding: 0;
max-width: none !important;

> * {
display: flex;
Expand All @@ -79,19 +86,23 @@ input.input {
margin: 0;
border: none;
padding: 0;
max-width: none !important;
transform: none !important;
}

input {
flex: 1 1 0;
margin: 0 !important;
padding: 0 !important;
border: none !important;
max-width: none !important;
width: auto !important;
height: 22px !important;
outline: none !important;
box-shadow: none !important;
line-height: 22px;
color: inherit;
opacity: 1 !important;
}
}

Expand All @@ -103,7 +114,8 @@ input.input {

:global(.custom__input) {
flex: 1 1 0;
display: flex;
display: flex !important;
max-width: none !important;
}

:global(.custom__placeholder) {
Expand All @@ -115,9 +127,12 @@ input.input {
}

:global(.custom__menu) {
margin: 1px 0 0;
left: -1px;
right: -1px;
margin: 2px 0 0;
border: 1px solid $control-border-color;
border-radius: 0;
width: auto;
box-shadow: none;
}

Expand All @@ -141,8 +156,8 @@ input.input {
}
}

:global(.custom__option--is-selected) {
background-color: #229174 !important;
:global(.custom__option--is-focused) {
background-color: $primary-text-color !important;
color: #fff;
}
}
18 changes: 18 additions & 0 deletions src/components/ToastrMessage/index.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import React from "react";
import { toastr } from "react-redux-toastr";
import PT from "prop-types";
import cn from "classnames";
import { TOAST_DEFAULT_TIMEOUT } from "constants/index.js";
import styles from "./styles.module.scss";

/**
Expand Down Expand Up @@ -38,3 +40,19 @@ ToastrMessage.propTypes = {
};

export default ToastrMessage;

/**
* Creates a redux toastr message with the specified type and contents.
*
* @param {string|Object} message
* @param {'info'|'success'|'warning'|'error'} type
*/
export function makeToast(message, type = "error") {
const component =
typeof message === "string" ? (
<ToastrMessage message={message} type={type} />
) : (
<ToastrMessage type={type}>{message}</ToastrMessage>
);
toastr[type]("", { component, options: { timeOut: TOAST_DEFAULT_TIMEOUT } });
}
Loading