Commit cce52b42 authored by Nicolas Delebecque's avatar Nicolas Delebecque
Browse files

Merge branch '372-default-account' of framagit.org:nicodel/kresus into 372-default-account

parents 0b0f968a ee0c7a93
Pipeline #357316 passed with stage
in 4 minutes and 42 seconds
......@@ -19,7 +19,7 @@
- prettier
overrides:
- files: '**/*.ts'
- files: ['**/*.ts', '**/*.tsx']
parser: '@typescript-eslint/parser'
parserOptions:
ecmaVersion: 2018
......
import React from 'react';
import { connect } from 'react-redux';
import { translate as $t, displayLabel } from '../../helpers';
import { translate as $t, displayLabel, notify } from '../../helpers';
import { get, actions } from '../../store';
import { DISABLE_MODAL_SLUG } from './disable-access-modal';
......@@ -23,13 +23,21 @@ export default connect(
return {
handleSyncAccounts: () => actions.runAccountsSync(dispatch, props.accessId),
handleDeleteAccess: () => actions.deleteAccess(dispatch, props.accessId),
setAccessCustomLabel(oldCustomLabel, customLabel) {
actions.updateAccess(
dispatch,
props.accessId,
{ customLabel },
{ customLabel: oldCustomLabel }
);
async setAccessCustomLabel(oldCustomLabel, customLabel) {
try {
await actions.updateAccess(
dispatch,
props.accessId,
{ customLabel },
{ customLabel: oldCustomLabel }
);
} catch (error) {
notify.error(
$t('client.general.update_fail', {
error: error.message,
})
);
}
},
handleOpenEditModal() {
actions.showModal(dispatch, EDIT_ACCESS_MODAL_SLUG, props.accessId);
......
import React from 'react';
import { connect } from 'react-redux';
import { translate as $t, displayLabel } from '../../helpers';
import { translate as $t, displayLabel, notify } from '../../helpers';
import { actions, get } from '../../store';
import { Popconfirm } from '../ui';
......@@ -10,17 +10,25 @@ import LabelComponent from '../ui/label';
const AccountLabelComponent = connect(null, (dispatch, props) => {
return {
setCustomLabel(label) {
actions.updateAccount(
dispatch,
props.item.id,
{
customLabel: label,
},
{
customLabel: props.item.customLabel,
}
);
async setCustomLabel(label) {
try {
await actions.updateAccount(
dispatch,
props.item.id,
{
customLabel: label,
},
{
customLabel: props.item.customLabel,
}
);
} catch (error) {
notify.error(
$t('client.general.update_fail', {
error: error.message,
})
);
}
},
getLabel() {
return props.item.label.trim();
......
......@@ -81,7 +81,7 @@ function categoryNotFoundMessage() {
const SearchCategorySelect = connect(
state => {
return {
defaultValue: get.searchFields(state).categoryIds,
value: get.searchFields(state).categoryIds,
categories: get.categories(state),
};
},
......@@ -109,8 +109,8 @@ const SearchCategorySelect = connect(
noOptionsMessage={categoryNotFoundMessage}
onChange={props.handleChange}
options={options}
values={props.defaultValue}
placeholder={$t('client.search.category_placeholder')}
values={props.value}
placeholder={$t('client.search.category_placeholder', props.value.length)}
/>
);
});
......
import React, { useState, useEffect, useImperativeHandle, ChangeEvent } from 'react';
import React, { useState, useEffect, useImperativeHandle, ChangeEvent, useCallback } from 'react';
import { Range } from 'rc-slider';
......@@ -21,13 +21,13 @@ interface ExposedMethods {
}
const MinMaxInput = React.forwardRef<ExposedMethods, MinMaxInputProps>((props, ref) => {
let [lowText, setLowText] = useState<string>(`${props.min}`);
let [lowNumber, setLowNumber] = useState<number>(props.min);
let [highText, setHighText] = useState<string>(`${props.max}`);
let [highNumber, setHighNumber] = useState<number>(props.max);
const [lowText, setLowText] = useState<string>(`${props.min}`);
const [lowNumber, setLowNumber] = useState<number>(props.min);
const [highText, setHighText] = useState<string>(`${props.max}`);
const [highNumber, setHighNumber] = useState<number>(props.max);
let [prevMin, setPrevMin] = useState<number>(props.min);
let [prevMax, setPrevMax] = useState<number>(props.max);
const [prevMin, setPrevMin] = useState<number>(props.min);
const [prevMax, setPrevMax] = useState<number>(props.max);
// On every mount/update, if the previous value of props.{min, max} doesn't
// match what we've had, then we've *probably* changed the view. It's
......@@ -65,87 +65,112 @@ const MinMaxInput = React.forwardRef<ExposedMethods, MinMaxInputProps>((props, r
},
}));
const { onChange } = props;
// Aggregated helpers.
let updateLow = (newVal: number) => {
if (newVal !== lowNumber) {
setLowNumber(newVal);
setLowText(`${newVal}`);
props.onChange(newVal, highNumber);
}
};
let updateHigh = (newVal: number) => {
if (newVal !== highNumber) {
setHighNumber(newVal);
setHighText(`${newVal}`);
props.onChange(lowNumber, newVal);
}
};
let validateLow = (newLow: number) => {
// Don't allow a value larger than the highValue or smaller than the
// props.min.
updateLow(Math.min(highNumber, Math.max(newLow, props.min)));
};
let validateHigh = (newHigh: number) => {
// Don't allow a value smaller than the lowValue or bigger than the
// props.max.
updateHigh(Math.max(lowNumber, Math.min(newHigh, props.max)));
};
const updateLow = useCallback(
(newVal: number) => {
if (newVal !== lowNumber) {
setLowNumber(newVal);
setLowText(`${newVal}`);
onChange(newVal, highNumber);
}
},
[setLowNumber, lowNumber, setLowText, highNumber, onChange]
);
const updateHigh = useCallback(
(newVal: number) => {
if (newVal !== highNumber) {
setHighNumber(newVal);
setHighText(`${newVal}`);
onChange(lowNumber, newVal);
}
},
[setHighNumber, setHighText, lowNumber, onChange, highNumber]
);
const validateLow = useCallback(
(newLow: number) => {
// Don't allow a value larger than the highValue or smaller than the
// props.min.
updateLow(Math.min(highNumber, Math.max(newLow, props.min)));
},
[highNumber, updateLow, props.min]
);
const validateHigh = useCallback(
(newHigh: number) => {
// Don't allow a value smaller than the lowValue or bigger than the
// props.max.
updateHigh(Math.max(lowNumber, Math.min(newHigh, props.max)));
},
[updateHigh, lowNumber, props.max]
);
// Event handlers.
let handleLow = (event: ChangeEvent<HTMLInputElement>) => {
let newVal = event.target.value;
let newLow = Number.parseFloat(newVal);
if (Number.isNaN(newLow)) {
// Just update the text field; the user might be typing something.
setLowText(newVal);
} else {
// Update in real-time, from a click on the arrows or a real-time
// input.
validateLow(newLow);
}
};
const handleLow = useCallback(
(event: ChangeEvent<HTMLInputElement>) => {
const newVal = event.target.value;
const newLow = Number.parseFloat(newVal);
if (Number.isNaN(newLow)) {
// Just update the text field; the user might be typing something.
setLowText(newVal);
} else {
// Update in real-time, from a click on the arrows or a real-time
// input.
validateLow(newLow);
}
},
[setLowText, validateLow]
);
let handleLowBlur = () => {
let newLow = Number.parseFloat(lowText);
const handleLowBlur = useCallback(() => {
const newLow = Number.parseFloat(lowText);
if (Number.isNaN(newLow)) {
// Reset to the previous value.
setLowText(`${lowNumber}`);
} else {
validateLow(newLow);
}
};
let handleHigh = (event: ChangeEvent<HTMLInputElement>) => {
let newVal = event.target.value;
let newHigh = Number.parseFloat(newVal);
if (Number.isNaN(newHigh)) {
// Just update the text field; the user might be typing something.
setHighText(newVal);
} else {
// Update in real-time, from a click on the arrows or a real-time
// input.
validateHigh(newHigh);
}
};
}, [lowText, setLowText, lowNumber, validateLow]);
const handleHigh = useCallback(
(event: ChangeEvent<HTMLInputElement>) => {
const newVal = event.target.value;
const newHigh = Number.parseFloat(newVal);
if (Number.isNaN(newHigh)) {
// Just update the text field; the user might be typing something.
setHighText(newVal);
} else {
// Update in real-time, from a click on the arrows or a real-time
// input.
validateHigh(newHigh);
}
},
[setHighText, validateHigh]
);
let handleHighBlur = () => {
let newHigh = Number.parseFloat(highText);
const handleHighBlur = useCallback(() => {
const newHigh = Number.parseFloat(highText);
if (Number.isNaN(newHigh)) {
// Reset to the previous value.
setHighText(`${highNumber}`);
} else {
validateHigh(newHigh);
}
};
let handleSlider = (values: [number, number]) => {
// Only one slider value can be changed at a time.
if (values[0] !== Infinity && values[0] !== lowNumber) {
updateLow(values[0]);
} else if (values[1] !== Infinity && values[1] !== highNumber) {
updateHigh(values[1]);
}
};
}, [highText, setHighText, validateHigh, highNumber]);
const handleSlider = useCallback(
(values: [number, number]) => {
// Only one slider value can be changed at a time.
if (values[0] !== Infinity && values[0] !== lowNumber) {
updateLow(values[0]);
} else if (values[1] !== Infinity && values[1] !== highNumber) {
updateHigh(values[1]);
}
},
[highNumber, lowNumber, updateHigh, updateLow]
);
return (
<div className="min-max-input">
......
body.dark form.search .Select input[type='checkbox'] {
border-color: #000000;
background-color: transparent;
}
body.dark form.search .Select input[type='checkbox']:checked {
color: #FFFFFF;
background-color: #666666;
}
.Select__menu.Select__multiple-select-menu {
background-color: transparent;
color: inherit;
cursor: default;
display: block;
font-size: inherit;
padding: 8px 6px;
width: 100%;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
box-sizing: border-box;
}
.Select__multiple-select-menu label {
font-weight: bold;
padding-left: 0 !important;
}
.Select__multiple-select-menu input + label {
margin-left: 0.5em;
}
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import Select, { createFilter } from 'react-select';
import Select, { createFilter, components } from 'react-select';
import { translate as $t } from '../../helpers';
import { get } from '../../store';
import { assert } from '../../helpers';
import './multiple-select.css';
const REACT_SELECT_FILTER = createFilter({
ignoreCase: true,
......@@ -14,21 +15,103 @@ const REACT_SELECT_FILTER = createFilter({
stringify: ({ label }) => label.toString(),
});
const Menu = (props: components.MenuProps) => {
const { className, cx, innerProps, innerRef, children, selectProps } = props;
const selectedOptions = props.getValue();
const isAllSelected = !props.options.some(o => !selectedOptions.includes(o));
function handleClick() {
if (isAllSelected) {
props.setValue([], 'set-value');
} else {
props.setValue(props.options, 'set-value');
}
}
return (
<components.Menu {...props}>
<div
ref={innerRef}
className={cx(
{
menu: true,
'multiple-select-menu': true,
},
className
)}
{...innerProps}
onClick={handleClick}
onTouchEnd={handleClick}>
<input type="checkbox" checked={isAllSelected} readOnly={true} />
<label>{selectProps.selectAllMessage}</label>
</div>
{children}
</components.Menu>
);
};
const ValueContainer = ({ children, ...props }: components.ValueContainerProps) => {
const { hasValue, selectProps } = props;
const filter = child => (!hasValue || child.type === components.Input ? child : null);
return (
<components.ValueContainer {...props}>
{React.Children.map(children, filter)}
{hasValue && !selectProps.inputValue && selectProps.placeholder}
</components.ValueContainer>
);
};
const Option = (props: components.OptionProps) => {
const { cx } = props;
return (
<components.Option {...props}>
<div
className={cx({
option: true,
'multiple-select-menu': true,
})}>
<input type="checkbox" checked={props.isSelected} readOnly={true} />
<label>{props.data.label}</label>
</div>
</components.Option>
);
};
interface MultiSelectOptionProps {
label: string;
value: string | number;
}
interface MultipleSelectProps {
// A string describing the classes to apply to the select.
className: string;
// A function returning the text to display when no such options are found,
// in fuzzy mode.
noOptionsMessage: () => string;
// A callback to be called when the user selects a new value.
onChange: (value: any[] | null) => void;
// An array of options in the select.
options?: MultiSelectOptionProps[];
// A text to display when nothing is selected.
placeholder?: string;
// A boolean telling whether the field is required.
required: boolean;
// The value that's selected at start.
values?: Array<string | number>;
}
const MultipleSelect = connect(state => {
let isSmallScreen = get.isSmallScreen(state);
const isSmallScreen = get.isSmallScreen(state);
return {
isSearchable: !isSmallScreen,
};
})(props => {
let handleChange = event => {
let values = [];
if (event instanceof Array) {
values = event.map(e => e.value);
} else {
// No values are selected.
assert(event === null);
}
})((props: MultipleSelectProps) => {
function handleChange(event: any[] | null): void {
const values: Array<string | number> = event ? event.map(e => e.value) : [];
const currentValues = props.values || [];
if (
values.length !== currentValues.length ||
......@@ -37,17 +120,25 @@ const MultipleSelect = connect(state => {
) {
props.onChange(values);
}
};
}
let { className, options, placeholder, required, values } = props;
const { options, placeholder, required, values } = props;
className += ' Select';
let className = `${props.className} Select`;
if (required) {
className += values ? ' valid-fuzzy' : ' invalid-fuzzy';
}
const currentValues =
values instanceof Array ? options.filter(opt => values.includes(opt.value)) : [];
let currentValues: MultiSelectOptionProps[] = [];
if (values && options) {
currentValues = options.filter(opt => (values || []).includes(opt.value));
}
const customComponents = {
ValueContainer,
Option,
Menu,
};
return (
<Select
......@@ -61,41 +152,15 @@ const MultipleSelect = connect(state => {
options={options}
placeholder={placeholder}
value={currentValues}
isSearchable={props.isSearchable}
isMulti={true}
hideSelectedOptions={false}
closeMenuOnSelect={false}
selectAllMessage={$t('client.general.select_all')}
components={customComponents}
/>
);
});
MultipleSelect.propTypes = {
// A string describing the classes to apply to the select.
className: PropTypes.string.isRequired,
// A function returning the text to display when no such options are found,
// in fuzzy mode.
noOptionsMessage: PropTypes.func,
// A callback to be called when the user selects a new value.
onChange: PropTypes.func.isRequired,
// An array of options in the select.
options: PropTypes.arrayOf(
PropTypes.shape({
label: PropTypes.string.isRequired,
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
})
),
// A text to display when nothing is selected.
placeholder: PropTypes.string,
// A boolean telling whether the field is required.
required: PropTypes.bool.isRequired,
// The value that's selected at start.
values: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.number, PropTypes.string])),
};
MultipleSelect.defaultProps = {
required: false,
className: '',
......
......@@ -47,6 +47,8 @@ import {
RUN_OPERATIONS_SYNC,
RUN_APPLY_BULKEDIT,
UPDATE_ALERT,
UPDATE_ACCESS_AND_FETCH,
UPDATE_ACCESS,
} from './actions';
import StaticBanks from '../../shared/banks.json';
......@@ -207,6 +209,24 @@ const basic = {
accountId,
};
},
updateAndFetchAccess(accessId, newFields = {}, results = null) {
return {
type: UPDATE_ACCESS_AND_FETCH,
accessId,
newFields,
results,
};
},
updateAccess(accessId, newFields) {
return {
type: UPDATE_ACCESS,
accessId,
newFields,
oldFields: {},
};
},
};
const fail = {},
......@@ -255,6 +275,65 @@ export function createAccess(
};
}
export function updateAndFetchAccess(accessId, login, password, customFields) {
let newFields = {
login,