@@ -0,0 +1,88 @@
|
||||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import selectors from '../../../selectors';
|
||||
import entryActions from '../../../entry-actions';
|
||||
import { buildCustomFieldValueId } from '../../../models/CustomFieldValue';
|
||||
import { isListArchiveOrTrash } from '../../../utils/record-helpers';
|
||||
import { BoardMembershipRoles } from '../../../constants/Enums';
|
||||
import ValueField from './ValueField';
|
||||
|
||||
import styles from './CustomField.module.scss';
|
||||
|
||||
const CustomField = React.memo(({ id, customFieldGroupId }) => {
|
||||
const selectCustomFieldById = useMemo(() => selectors.makeSelectCustomFieldById(), []);
|
||||
const selectCustomFieldValueById = useMemo(() => selectors.makeSelectCustomFieldValueById(), []);
|
||||
const selectListById = useMemo(() => selectors.makeSelectListById(), []);
|
||||
|
||||
const { cardId } = useSelector(selectors.selectPath);
|
||||
const customField = useSelector((state) => selectCustomFieldById(state, id));
|
||||
|
||||
const customFieldValue = useSelector((state) =>
|
||||
selectCustomFieldValueById(
|
||||
state,
|
||||
buildCustomFieldValueId({
|
||||
cardId,
|
||||
customFieldGroupId,
|
||||
customFieldId: id,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
const canEdit = useSelector((state) => {
|
||||
const { listId } = selectors.selectCurrentCard(state);
|
||||
const list = selectListById(state, listId);
|
||||
|
||||
if (isListArchiveOrTrash(list)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const boardMembership = selectors.selectCurrentUserMembershipForCurrentBoard(state);
|
||||
return !!boardMembership && boardMembership.role === BoardMembershipRoles.EDITOR;
|
||||
});
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const handleValueUpdate = useCallback(
|
||||
(content) => {
|
||||
if (content) {
|
||||
dispatch(
|
||||
entryActions.updateCustomFieldValue(cardId, customFieldGroupId, id, {
|
||||
content,
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
dispatch(entryActions.deleteCustomFieldValue(cardId, customFieldGroupId, id));
|
||||
}
|
||||
},
|
||||
[id, customFieldGroupId, cardId, dispatch],
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={styles.name}>{customField.name}</div>
|
||||
{canEdit ? (
|
||||
<ValueField
|
||||
defaultValue={customFieldValue && customFieldValue.content}
|
||||
disabled={!customField.isPersisted}
|
||||
onUpdate={handleValueUpdate}
|
||||
/>
|
||||
) : (
|
||||
<div className={styles.value}>{customFieldValue ? customFieldValue.content : '\u00A0'}</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
CustomField.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
customFieldGroupId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default CustomField;
|
||||
@@ -0,0 +1,28 @@
|
||||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
:global(#app) {
|
||||
.name {
|
||||
color: #6b808c;
|
||||
font-size: 13px;
|
||||
line-height: 1;
|
||||
margin-bottom: 6px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.value {
|
||||
background: rgba(9, 30, 66, 0.04);
|
||||
border: 1px solid transparent;
|
||||
border-radius: 3px;
|
||||
color: #17394d;
|
||||
line-height: 20px;
|
||||
opacity: 0.45;
|
||||
overflow: hidden;
|
||||
padding: 8px 12px;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
import React, { useCallback, useRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useDidUpdate, usePrevious, useToggle } from '../../../lib/hooks';
|
||||
import { Input } from '../../../lib/custom-ui';
|
||||
|
||||
import { useEscapeInterceptor, useField, useNestedRef } from '../../../hooks';
|
||||
|
||||
import styles from './ValueField.module.scss';
|
||||
|
||||
const ValueField = React.memo(({ defaultValue, onUpdate, ...props }) => {
|
||||
const prevDefaultValue = usePrevious(defaultValue);
|
||||
const [value, handleChange, setValue] = useField(defaultValue || '');
|
||||
const [blurFieldState, blurField] = useToggle();
|
||||
|
||||
const [fieldRef, handleFieldRef] = useNestedRef('inputRef');
|
||||
const isFocusedRef = useRef(false);
|
||||
|
||||
const handleEscape = useCallback(() => {
|
||||
setValue(defaultValue || '');
|
||||
blurField();
|
||||
}, [defaultValue, setValue, blurField]);
|
||||
|
||||
const [activateEscapeInterceptor, deactivateEscapeInterceptor] =
|
||||
useEscapeInterceptor(handleEscape);
|
||||
|
||||
const handleFocus = useCallback(() => {
|
||||
activateEscapeInterceptor();
|
||||
isFocusedRef.current = true;
|
||||
}, [activateEscapeInterceptor]);
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(event) => {
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
fieldRef.current.blur();
|
||||
}
|
||||
},
|
||||
[fieldRef],
|
||||
);
|
||||
|
||||
const handleBlur = useCallback(() => {
|
||||
deactivateEscapeInterceptor();
|
||||
isFocusedRef.current = false;
|
||||
|
||||
const cleanValue = value.trim() || null;
|
||||
|
||||
if (cleanValue !== defaultValue) {
|
||||
onUpdate(cleanValue);
|
||||
}
|
||||
}, [defaultValue, onUpdate, value, deactivateEscapeInterceptor]);
|
||||
|
||||
useDidUpdate(() => {
|
||||
if (!isFocusedRef.current && defaultValue !== prevDefaultValue) {
|
||||
setValue(defaultValue || '');
|
||||
}
|
||||
}, [defaultValue, prevDefaultValue]);
|
||||
|
||||
useDidUpdate(() => {
|
||||
fieldRef.current.blur();
|
||||
}, [blurFieldState]);
|
||||
|
||||
return (
|
||||
<Input
|
||||
{...props} // eslint-disable-line react/jsx-props-no-spreading
|
||||
fluid
|
||||
ref={handleFieldRef}
|
||||
value={value}
|
||||
maxLength={512}
|
||||
className={styles.field}
|
||||
onFocus={handleFocus}
|
||||
onKeyDown={handleKeyDown}
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
ValueField.propTypes = {
|
||||
defaultValue: PropTypes.string,
|
||||
onUpdate: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
ValueField.defaultProps = {
|
||||
defaultValue: undefined,
|
||||
};
|
||||
|
||||
export default ValueField;
|
||||
@@ -0,0 +1,18 @@
|
||||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
:global(#app) {
|
||||
.field input {
|
||||
background: rgba(9, 30, 66, 0.04);
|
||||
border: 1px solid transparent;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
&:focus {
|
||||
background: #fff;
|
||||
border-color: #5ba4cf;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
import CustomField from './CustomField';
|
||||
|
||||
export default CustomField;
|
||||
Reference in New Issue
Block a user