feat: Version 2

Closes #627, closes #1047
This commit is contained in:
Maksim Eltyshev
2025-05-10 02:09:06 +02:00
parent ad7fb51cfa
commit 2ee1166747
1557 changed files with 76832 additions and 47042 deletions
@@ -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;