mirror of
https://github.com/ToolJet/ToolJet
synced 2026-04-21 21:47:17 +00:00
Add TagsV2 support in Table component
This commit is contained in:
parent
8186f3d061
commit
dbd35e942e
16 changed files with 645 additions and 8 deletions
|
|
@ -105,6 +105,7 @@ export const PropertiesTabElements = ({
|
|||
{ label: 'Date Picker', value: 'datepicker' },
|
||||
{ label: 'Select', value: 'select' },
|
||||
{ label: 'MultiSelect', value: 'newMultiSelect' },
|
||||
{ label: 'Tags', value: 'tagsV2' },
|
||||
{ label: 'Boolean', value: 'boolean' },
|
||||
{ label: 'Image', value: 'image' },
|
||||
{ label: 'Link', value: 'link' },
|
||||
|
|
@ -449,7 +450,7 @@ export const PropertiesTabElements = ({
|
|||
component={component}
|
||||
/>
|
||||
)}
|
||||
{['select', 'newMultiSelect'].includes(column.columnType) && (
|
||||
{['select', 'newMultiSelect', 'tagsV2'].includes(column.columnType) && (
|
||||
<OptionsList
|
||||
column={column}
|
||||
props={props}
|
||||
|
|
|
|||
|
|
@ -140,6 +140,7 @@ export const StylesTabElements = ({
|
|||
'select',
|
||||
'text',
|
||||
'newMultiSelect',
|
||||
'tagsV2',
|
||||
'datepicker',
|
||||
].includes(column.columnType) && (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -97,6 +97,7 @@ export const ValidationProperties = ({
|
|||
case 'dropdown':
|
||||
case 'select':
|
||||
case 'newMultiSelect':
|
||||
case 'tagsV2':
|
||||
return [
|
||||
{
|
||||
property: 'customRule',
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ import { ProgramaticallyHandleProperties } from '../ProgramaticallyHandlePropert
|
|||
import { resolveReferences } from '@/_helpers/utils';
|
||||
import { Button as ButtonComponent } from '@/components/ui/Button/Button';
|
||||
import { unset } from 'lodash';
|
||||
import ToggleGroup from '@/ToolJetUI/SwitchGroup/ToggleGroup';
|
||||
import ToggleGroupItem from '@/ToolJetUI/SwitchGroup/ToggleGroupItem';
|
||||
|
||||
export const OptionsList = ({
|
||||
column,
|
||||
|
|
@ -263,9 +265,40 @@ export const OptionsList = ({
|
|||
};
|
||||
|
||||
items.push({
|
||||
title: 'Options',
|
||||
title: column.columnType === 'tagsV2' ? 'Tags' : 'Options',
|
||||
children: (
|
||||
<div className="d-flex custom-gap-7 flex-column">
|
||||
{column.columnType === 'tagsV2' && (
|
||||
<>
|
||||
<div className="field d-flex custom-gap-12 align-items-center align-self-stretch justify-content-between px-3">
|
||||
<label className="form-label">Sort tags</label>
|
||||
<ToggleGroup
|
||||
onValueChange={(value) => onColumnItemChange(index, 'sortTags', value)}
|
||||
defaultValue={column?.sortTags || 'none'}
|
||||
style={{ width: '58%' }}
|
||||
>
|
||||
<ToggleGroupItem value="none">None</ToggleGroupItem>
|
||||
<ToggleGroupItem value="a-z">a-z</ToggleGroupItem>
|
||||
<ToggleGroupItem value="z-a">z-a</ToggleGroupItem>
|
||||
</ToggleGroup>
|
||||
</div>
|
||||
<ProgramaticallyHandleProperties
|
||||
label="Allow multiple selection"
|
||||
currentState={currentState}
|
||||
index={index}
|
||||
darkMode={darkMode}
|
||||
callbackFunction={onColumnItemChange}
|
||||
property="allowMultipleSelection"
|
||||
props={column}
|
||||
component={component}
|
||||
paramMeta={{
|
||||
type: 'toggle',
|
||||
displayName: 'Allow multiple selection',
|
||||
}}
|
||||
paramType="properties"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<ProgramaticallyHandleProperties
|
||||
label="Auto assign colors"
|
||||
currentState={currentState}
|
||||
|
|
@ -400,7 +433,7 @@ export const OptionsList = ({
|
|||
className="tw-w-full mt-2"
|
||||
width="100%"
|
||||
>
|
||||
Add new option
|
||||
{column.columnType === 'tagsV2' ? 'Add new tag' : 'Add new option'}
|
||||
</ButtonComponent>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ const getColumnTypeDisplayText = (columnType) => {
|
|||
boolean: 'Boolean',
|
||||
select: 'Select',
|
||||
newMultiSelect: 'Multiselect',
|
||||
tagsV2: 'Tags',
|
||||
json: 'JSON',
|
||||
markdown: 'Markdown',
|
||||
html: 'HTML',
|
||||
|
|
|
|||
|
|
@ -0,0 +1,29 @@
|
|||
import React from 'react';
|
||||
|
||||
const TagsV2TypeIcon = ({ fill = '#ACB2B9', width = '16', className = '', viewBox = '0 0 16 16', style, height }) => (
|
||||
<svg
|
||||
className={className}
|
||||
width={width}
|
||||
height={height}
|
||||
viewBox={viewBox}
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
style={style}
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M1.33398 11.0618V13.1027C1.33398 13.9482 2.01937 14.6336 2.86483 14.6336H10.0769C10.5001 14.6336 10.9043 14.4584 11.1938 14.1497L14.3913 10.739C14.7593 10.3465 14.7593 9.7356 14.3913 9.34303L13.8315 8.746L11.9383 10.7655C11.456 11.28 10.7822 11.5719 10.0769 11.5719H2.86483C2.29046 11.5719 1.76041 11.382 1.33398 11.0618Z"
|
||||
fill={fill}
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M2.86483 1.36621C2.01937 1.36621 1.33398 2.05159 1.33398 2.89706V9.02045C1.33398 9.86592 2.01937 10.5513 2.86483 10.5513H10.0769C10.5001 10.5513 10.9043 10.3762 11.1938 10.0675L14.3913 6.65676C14.7593 6.26419 14.7593 5.65332 14.3913 5.26075L11.1938 1.85005C10.9043 1.54135 10.5001 1.36621 10.0769 1.36621H2.86483Z"
|
||||
fill={fill}
|
||||
/>
|
||||
<circle cx="5" cy="5.9" r="1.2" fill="white" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default TagsV2TypeIcon;
|
||||
|
|
@ -12,6 +12,7 @@ export { default as MarkdownTypeIcon } from './MarkdownTypeIcon';
|
|||
export { default as HTMLTypeIcon } from './HTMLTypeIcon';
|
||||
export { default as BadgeTypeIcon } from './BadgeTypeIcon';
|
||||
export { default as TagsTypeIcon } from './TagsTypeIcon';
|
||||
export { default as TagsV2TypeIcon } from './TagsV2TypeIcon';
|
||||
export { default as RadioTypeIcon } from './RadioTypeIcon';
|
||||
export { default as RatingTypeIcon } from './RadioTypeIcon';
|
||||
export { default as ButtonTypeIcon } from './ButtonTypeIcon';
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ export const useColumnManager = ({ component, paramUpdated, currentState }) => {
|
|||
let modifiedColumn = { ...column };
|
||||
|
||||
// Handle select/multiselect default options
|
||||
if (property === 'columnType' && (value === 'select' || value === 'newMultiSelect')) {
|
||||
if (property === 'columnType' && (value === 'select' || value === 'newMultiSelect' || value === 'tagsV2')) {
|
||||
if (modifiedColumn.options?.length > 0) {
|
||||
modifiedColumn.options = modifiedColumn.options.map((opt) => {
|
||||
const { makeDefaultOption, ...rest } = opt;
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import {
|
|||
StringTypeIcon,
|
||||
BadgeTypeIcon,
|
||||
TagsTypeIcon,
|
||||
TagsV2TypeIcon,
|
||||
RadioTypeIcon,
|
||||
RatingTypeIcon,
|
||||
ButtonTypeIcon,
|
||||
|
|
@ -56,6 +57,8 @@ export const getColumnIcon = (columnType) => {
|
|||
return BadgeTypeIcon;
|
||||
case 'tags':
|
||||
return TagsTypeIcon;
|
||||
case 'tagsV2':
|
||||
return TagsV2TypeIcon;
|
||||
case 'rating':
|
||||
return RatingTypeIcon;
|
||||
case 'button':
|
||||
|
|
|
|||
|
|
@ -0,0 +1,425 @@
|
|||
import React, { useCallback, useMemo, useRef, useEffect, useState } from 'react';
|
||||
import Select from '@/_ui/Select';
|
||||
import SolidIcon from '@/_ui/Icon/SolidIcons';
|
||||
import { Checkbox } from '@/_ui/CheckBox/CheckBox';
|
||||
import OverlayTrigger from 'react-bootstrap/OverlayTrigger';
|
||||
import { isArray } from 'lodash';
|
||||
import TagsInputMenuList from '@/AppBuilder/Widgets/TagsInput/TagsInputMenuList';
|
||||
import defaultStyles from '@/_ui/Select/styles';
|
||||
|
||||
const COLORS = [
|
||||
'#40474D33',
|
||||
'#CE276133',
|
||||
'#6745E233',
|
||||
'#2576CE33',
|
||||
'#1A9C6D33',
|
||||
'#69AF2033',
|
||||
'#F3571733',
|
||||
'#EB2E3933',
|
||||
'#A438C033',
|
||||
'#405DE633',
|
||||
'#1E8FA333',
|
||||
'#34A94733',
|
||||
'#F1911933',
|
||||
];
|
||||
|
||||
const sortOptions = (opts, sortTags) => {
|
||||
if (!sortTags || sortTags === 'none') return opts;
|
||||
return [...opts].sort((a, b) => {
|
||||
const cmp = (a.label || '').localeCompare(b.label || '');
|
||||
return sortTags === 'a-z' ? cmp : -cmp;
|
||||
});
|
||||
};
|
||||
|
||||
const CustomOption = ({ innerRef, innerProps, children, isSelected, ...props }) => {
|
||||
const { label, value, data } = props;
|
||||
const { optionColors } = props.selectProps;
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={innerRef}
|
||||
{...innerProps}
|
||||
className="option-wrapper d-flex"
|
||||
style={{ backgroundColor: 'var(--cc-surface1-surface)' }}
|
||||
>
|
||||
{props.selectProps.isMulti ? (
|
||||
<Checkbox label="" isChecked={isSelected} onChange={(e) => e.stopPropagation()} key="" value={children} />
|
||||
) : (
|
||||
<div style={{ visibility: isSelected ? 'visible' : 'hidden' }}>
|
||||
<Checkbox label="" isChecked={isSelected} onChange={(e) => e.stopPropagation()} key="" value={children} />
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className="table-select-menu-pill"
|
||||
style={{
|
||||
background: optionColors?.[value] || 'var(--surfaces-surface-03)',
|
||||
color: data?.labelColor || 'var(--text-primary)',
|
||||
padding: '2px 6px',
|
||||
borderRadius: '6px',
|
||||
fontSize: '12px',
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const MultiValueRemove = ({ innerProps }) => <div {...innerProps} />;
|
||||
|
||||
const CustomMultiValueContainer = ({ children }) => (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
const DropdownIndicator = ({ selectProps }) => {
|
||||
return (
|
||||
<div
|
||||
className="cell-icon-display"
|
||||
style={{ alignSelf: 'center' }}
|
||||
onMouseDown={(e) => {
|
||||
const isOpen = selectProps.menuIsOpen;
|
||||
selectProps.onMenuOpen(!isOpen);
|
||||
|
||||
const tdElement = e.currentTarget.closest('td');
|
||||
if (tdElement) {
|
||||
const clickEvent = new MouseEvent('click', {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
view: window,
|
||||
});
|
||||
tdElement.dispatchEvent(clickEvent);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<SolidIcon
|
||||
name={selectProps.menuIsOpen ? 'arrowUpTriangle' : 'arrowDownTriangle'}
|
||||
width="16"
|
||||
height="16"
|
||||
fill="#6A727C"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const getOverlay = (value, darkMode) => {
|
||||
if (!isArray(value)) return <div />;
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{ maxWidth: '266px' }}
|
||||
className={`overlay-cell-table overlay-multiselect-table ${darkMode ? 'dark-theme' : ''}`}
|
||||
>
|
||||
{value.map((option) => (
|
||||
<span
|
||||
key={option.label || option}
|
||||
style={{
|
||||
padding: '2px 6px',
|
||||
background: 'var(--surfaces-surface-03)',
|
||||
borderRadius: '6px',
|
||||
color: 'var(--text-primary)',
|
||||
fontSize: '12px',
|
||||
}}
|
||||
>
|
||||
{option.label || option}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* TagsRenderer — creatable tags dropdown for the Table TagsV2 column.
|
||||
*
|
||||
* Distinct from SelectRenderer: users can create new tag options at runtime
|
||||
* via TagsInputMenuList, tags are optionally auto-colored, and the option
|
||||
* list can be sorted. Borrows only control + valueContainer styles from
|
||||
* SelectRenderer via _sharedStyles; all other styling is local.
|
||||
*
|
||||
* Validation is owned by the adapter; this component is pure UI.
|
||||
*/
|
||||
export const TagsRenderer = ({
|
||||
options,
|
||||
value,
|
||||
onChange,
|
||||
defaultOptionsList = [],
|
||||
isMulti,
|
||||
autoAssignColors = true,
|
||||
sortTags = 'none',
|
||||
placeholder,
|
||||
disabled,
|
||||
className,
|
||||
darkMode,
|
||||
textColor = '',
|
||||
horizontalAlignment = 'left',
|
||||
optionsLoadingState = false,
|
||||
isEditable,
|
||||
isNewRow,
|
||||
isFocused,
|
||||
setIsFocused,
|
||||
menuIsOpen,
|
||||
isValid = true,
|
||||
validationError,
|
||||
}) => {
|
||||
const [runtimeTags, setRuntimeTags] = useState([]);
|
||||
|
||||
const allOptions = useMemo(() => [...options, ...runtimeTags], [options, runtimeTags]);
|
||||
const sortedOptions = useMemo(() => sortOptions(allOptions, sortTags), [allOptions, sortTags]);
|
||||
|
||||
const optionColors = useMemo(
|
||||
() =>
|
||||
allOptions.reduce((acc, option, index) => {
|
||||
acc[option.value] =
|
||||
option.optionColor || (autoAssignColors ? COLORS[index % COLORS.length] : 'var(--surfaces-surface-03)');
|
||||
return acc;
|
||||
}, {}),
|
||||
[allOptions, autoAssignColors]
|
||||
);
|
||||
|
||||
const handleCreate = useCallback(
|
||||
(newLabel) => {
|
||||
const trimmed = (newLabel || '').trim();
|
||||
if (!trimmed) return;
|
||||
const newTag = { label: trimmed, value: trimmed };
|
||||
setRuntimeTags((prev) => (prev.some((t) => t.value === trimmed) ? prev : [...prev, newTag]));
|
||||
if (isMulti) {
|
||||
const currentValues = isArray(value) ? value.map((v) => (v && typeof v === 'object' ? v.value : v)) : [];
|
||||
onChange([...currentValues, trimmed]);
|
||||
} else {
|
||||
onChange(trimmed);
|
||||
}
|
||||
},
|
||||
[isMulti, value, onChange]
|
||||
);
|
||||
|
||||
const isValidNewOption = useCallback(
|
||||
(input) => {
|
||||
if (!input || !input.trim()) return false;
|
||||
const trimmed = input.trim().toLowerCase();
|
||||
return !allOptions.some((o) => o.value?.toLowerCase() === trimmed);
|
||||
},
|
||||
[allOptions]
|
||||
);
|
||||
|
||||
const containerRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isMulti && !isNewRow) return;
|
||||
const handleDocumentClick = (event) => {
|
||||
if (!containerRef.current?.contains(event.target)) {
|
||||
setIsFocused?.(false);
|
||||
}
|
||||
};
|
||||
document.addEventListener('mousedown', handleDocumentClick);
|
||||
return () => document.removeEventListener('mousedown', handleDocumentClick);
|
||||
}, [isMulti, isNewRow, setIsFocused]);
|
||||
|
||||
const customComponents = {
|
||||
MenuList: (props) => (
|
||||
<TagsInputMenuList
|
||||
{...props}
|
||||
allowNewTags
|
||||
inputValue={props.selectProps?.inputValue}
|
||||
optionsLoadingState={optionsLoadingState}
|
||||
darkMode={darkMode}
|
||||
allOptions={allOptions}
|
||||
onCreateTag={handleCreate}
|
||||
autoPickChipColor={autoAssignColors}
|
||||
/>
|
||||
),
|
||||
Option: CustomOption,
|
||||
DropdownIndicator: isEditable ? DropdownIndicator : null,
|
||||
...(isMulti && {
|
||||
MultiValueRemove,
|
||||
MultiValueContainer: CustomMultiValueContainer,
|
||||
}),
|
||||
};
|
||||
|
||||
const customStyles = useMemo(
|
||||
() => ({
|
||||
...defaultStyles(darkMode, '100%'),
|
||||
valueContainer: (provided) => ({
|
||||
...provided,
|
||||
...(isMulti && {
|
||||
marginBottom: '0',
|
||||
display: 'flex',
|
||||
flexWrap: 'no-wrap',
|
||||
overflow: 'hidden',
|
||||
flexDirection: 'row',
|
||||
}),
|
||||
justifyContent: horizontalAlignment,
|
||||
}),
|
||||
...(isMulti && {
|
||||
multiValue: (provided) => ({
|
||||
...provided,
|
||||
display: 'inline-block',
|
||||
marginRight: '4px',
|
||||
}),
|
||||
multiValueLabel: (provided, state) => {
|
||||
const option = state.data;
|
||||
return {
|
||||
...provided,
|
||||
padding: '2px 6px',
|
||||
background: optionColors?.[option.value] || 'var(--surfaces-surface-03)',
|
||||
borderRadius: '6px',
|
||||
color: option?.labelColor || textColor || 'var(--text-primary)',
|
||||
fontSize: '12px',
|
||||
};
|
||||
},
|
||||
}),
|
||||
singleValue: (provided, state) => {
|
||||
const option = state.data;
|
||||
return {
|
||||
...provided,
|
||||
padding: '2px 6px',
|
||||
background: optionColors?.[option.value] || 'var(--surfaces-surface-03)',
|
||||
borderRadius: '6px',
|
||||
color: option?.labelColor || textColor || 'var(--text-primary)',
|
||||
fontSize: '12px',
|
||||
};
|
||||
},
|
||||
menuList: (provided) => ({
|
||||
...provided,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
overflowY: 'auto',
|
||||
backgroundColor: 'var(--surfaces-surface-01)',
|
||||
padding: '4px',
|
||||
}),
|
||||
menu: (provided) => ({
|
||||
...provided,
|
||||
padding: '0',
|
||||
marginTop: '5px',
|
||||
borderRadius: '8px',
|
||||
boxShadow: 'var(--elevation-300-box-shadow)',
|
||||
border: '1px solid var(--border-weak)',
|
||||
}),
|
||||
}),
|
||||
[darkMode, isMulti, horizontalAlignment, textColor, optionColors]
|
||||
);
|
||||
|
||||
const defaultValue = useMemo(
|
||||
() =>
|
||||
defaultOptionsList.length >= 1
|
||||
? isMulti
|
||||
? defaultOptionsList
|
||||
: defaultOptionsList.slice(-1)[0]
|
||||
: isMulti
|
||||
? []
|
||||
: {},
|
||||
[isMulti, defaultOptionsList]
|
||||
);
|
||||
|
||||
const selectedValue = useMemo(() => {
|
||||
if (!value) return null;
|
||||
if (isMulti && value?.length) {
|
||||
if (!isArray(value)) return [];
|
||||
const resolved = value.map((val) => {
|
||||
const v = val && typeof val === 'object' ? val.value : val;
|
||||
const match = allOptions?.find((option) => option.value === v);
|
||||
if (match) return match;
|
||||
return { label: String(v), value: String(v) };
|
||||
});
|
||||
return sortOptions(resolved.filter(Boolean), sortTags);
|
||||
}
|
||||
const v = isArray(value) ? value[0] : value;
|
||||
if (!v) return null;
|
||||
const match = allOptions?.find((option) => option.value === (v && typeof v === 'object' ? v.value : v));
|
||||
if (match) return match;
|
||||
return {
|
||||
label: String(v && typeof v === 'object' ? v.value : v),
|
||||
value: String(v && typeof v === 'object' ? v.value : v),
|
||||
};
|
||||
}, [allOptions, value, isMulti, sortTags]);
|
||||
|
||||
const handleChange = useCallback(
|
||||
(newValue) => {
|
||||
setIsFocused(false);
|
||||
if (!isMulti && newValue === selectedValue?.value) {
|
||||
onChange('');
|
||||
} else {
|
||||
onChange(newValue);
|
||||
}
|
||||
},
|
||||
[isMulti, selectedValue, onChange, setIsFocused]
|
||||
);
|
||||
|
||||
const isOverflowing = useCallback(() => {
|
||||
if (!containerRef.current) return false;
|
||||
const valueContainer = containerRef.current.querySelector('.react-select__value-container');
|
||||
return valueContainer?.clientHeight > containerRef.current?.clientHeight;
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<OverlayTrigger
|
||||
placement="bottom"
|
||||
overlay={
|
||||
isMulti && (selectedValue?.length || defaultValue?.length) && !isFocused ? (
|
||||
getOverlay(selectedValue || defaultValue, darkMode)
|
||||
) : (
|
||||
<div />
|
||||
)
|
||||
}
|
||||
trigger={isMulti && !isFocused && isOverflowing() && ['hover', 'focus']}
|
||||
rootClose={true}
|
||||
>
|
||||
<>
|
||||
<div
|
||||
className="w-100 d-flex align-items-center"
|
||||
ref={containerRef}
|
||||
onClick={() => {
|
||||
if (isNewRow && isEditable) {
|
||||
setIsFocused((prev) => !prev);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Select
|
||||
options={sortedOptions}
|
||||
hasSearch
|
||||
isDisabled={disabled}
|
||||
className={className}
|
||||
components={customComponents}
|
||||
value={selectedValue}
|
||||
onMenuInputFocus={() => setIsFocused(true)}
|
||||
onChange={handleChange}
|
||||
useCustomStyles={true}
|
||||
styles={customStyles}
|
||||
defaultValue={defaultValue}
|
||||
placeholder={placeholder}
|
||||
isMulti={isMulti}
|
||||
hideSelectedOptions={false}
|
||||
isClearable={false}
|
||||
clearIndicator={false}
|
||||
darkMode={darkMode}
|
||||
menuIsOpen={menuIsOpen}
|
||||
isFocused={isFocused}
|
||||
optionColors={optionColors}
|
||||
creatable
|
||||
onCreateOption={handleCreate}
|
||||
formatCreateLabel={() => null}
|
||||
isValidNewOption={isValidNewOption}
|
||||
/>
|
||||
</div>
|
||||
{isEditable && !isValid && (
|
||||
<div
|
||||
onClick={() => {
|
||||
if (!isValid) setIsFocused(true);
|
||||
}}
|
||||
className="invalid-feedback d-block"
|
||||
>
|
||||
{validationError}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
</OverlayTrigger>
|
||||
);
|
||||
};
|
||||
|
||||
export default TagsRenderer;
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
import React, { useCallback, useState } from 'react';
|
||||
import { TagsRenderer } from '@/AppBuilder/Shared/DataTypes/renderers/TagsRenderer';
|
||||
import useStore from '@/AppBuilder/_stores/store';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
import { isArray } from 'lodash';
|
||||
import useTextColor from '../_hooks/useTextColor';
|
||||
|
||||
export const TagsV2Column = ({
|
||||
options,
|
||||
value,
|
||||
onChange,
|
||||
placeholder,
|
||||
disabled,
|
||||
className,
|
||||
darkMode,
|
||||
defaultOptionsList = [],
|
||||
textColor = '',
|
||||
allowMultipleSelection = true,
|
||||
sortTags = 'none',
|
||||
optionsLoadingState = false,
|
||||
horizontalAlignment = 'left',
|
||||
isEditable,
|
||||
column,
|
||||
isNewRow,
|
||||
autoAssignColors = true,
|
||||
id,
|
||||
}) => {
|
||||
const [isFocused, setIsFocused] = useState(false);
|
||||
|
||||
const validateWidget = useStore((state) => state.validateWidget, shallow);
|
||||
const cellTextColor = useTextColor(id, textColor);
|
||||
|
||||
const validationData = validateWidget({
|
||||
validationObject: {
|
||||
customRule: { value: column?.customRule },
|
||||
},
|
||||
widgetValue: value,
|
||||
customResolveObjects: { value },
|
||||
});
|
||||
const { isValid, validationError } = validationData;
|
||||
|
||||
const handleChange = useCallback(
|
||||
(newValue) => {
|
||||
if (allowMultipleSelection) {
|
||||
const arr = isArray(newValue) ? newValue : [];
|
||||
onChange(arr.map((v) => (v && typeof v === 'object' ? v.value : v)));
|
||||
} else {
|
||||
if (newValue == null || newValue === '') {
|
||||
onChange([]);
|
||||
return;
|
||||
}
|
||||
const v = typeof newValue === 'object' ? newValue.value : newValue;
|
||||
onChange([v]);
|
||||
}
|
||||
},
|
||||
[allowMultipleSelection, onChange]
|
||||
);
|
||||
|
||||
return (
|
||||
<TagsRenderer
|
||||
options={options}
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
placeholder={placeholder}
|
||||
disabled={disabled}
|
||||
className={className}
|
||||
darkMode={darkMode}
|
||||
defaultOptionsList={defaultOptionsList}
|
||||
textColor={cellTextColor}
|
||||
isMulti={allowMultipleSelection}
|
||||
optionsLoadingState={optionsLoadingState}
|
||||
horizontalAlignment={horizontalAlignment}
|
||||
isEditable={isEditable}
|
||||
isNewRow={isNewRow}
|
||||
autoAssignColors={autoAssignColors}
|
||||
isFocused={isFocused}
|
||||
setIsFocused={setIsFocused}
|
||||
isValid={isValid}
|
||||
validationError={validationError}
|
||||
menuIsOpen={isFocused || undefined}
|
||||
sortTags={sortTags}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
@ -7,6 +7,7 @@ export { LinkColumn } from './adapters/LinkColumnAdapter';
|
|||
export { ImageColumn } from './adapters/ImageColumnAdapter';
|
||||
export { DatepickerColumn } from './adapters/Datepicker';
|
||||
export { CustomSelectColumn } from './adapters/SelectColumnAdapter'; // Select & MultiSelect
|
||||
export { TagsV2Column } from './adapters/TagsV2ColumnAdapter'; // TagsV2
|
||||
export { JsonColumn } from './adapters/JsonColumnAdapter';
|
||||
export { MarkdownColumn } from './adapters/MarkdownColumnAdapter';
|
||||
export { HTMLColumn } from './adapters/HtmlColumnAdapter';
|
||||
|
|
|
|||
|
|
@ -177,7 +177,7 @@ export function AddNewRow({ id, hideAddNewRowPopup, darkMode, allColumns, fireEv
|
|||
'align-items-center flex-column': cell.column.columnDef.meta?.columnType === 'selector',
|
||||
'selector-column':
|
||||
cell.column.columnDef.meta?.columnType === 'selector' && cell.column.id === 'selection',
|
||||
'has-select': ['select', 'newMultiSelect'].includes(cell.column.columnDef.meta?.columnType),
|
||||
'has-select': ['select', 'newMultiSelect', 'tagsV2'].includes(cell.column.columnDef.meta?.columnType),
|
||||
isEditable: true,
|
||||
})}
|
||||
style={{ width: cell.column.getSize() }}
|
||||
|
|
|
|||
|
|
@ -107,7 +107,7 @@ export const TableRow = ({
|
|||
['text', 'string', undefined, 'number'].includes(cell.column.columnDef?.meta?.columnType) &&
|
||||
!contentWrap,
|
||||
'selector-column': cell.column.columnDef?.meta?.columnType === 'selector',
|
||||
'has-select': ['select', 'newMultiSelect'].includes(cell.column.columnDef?.meta?.columnType),
|
||||
'has-select': ['select', 'newMultiSelect', 'tagsV2'].includes(cell.column.columnDef?.meta?.columnType),
|
||||
'has-tags': cell.column.columnDef?.meta?.columnType === 'tags',
|
||||
'has-link': cell.column.columnDef?.meta?.columnType === 'link',
|
||||
'has-radio': cell.column.columnDef?.meta?.columnType === 'radio',
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import {
|
|||
LinkColumn,
|
||||
ImageColumn,
|
||||
CustomSelectColumn,
|
||||
TagsV2Column,
|
||||
TextColumn,
|
||||
JsonColumn,
|
||||
MarkdownColumn,
|
||||
|
|
@ -295,6 +296,52 @@ export default function generateColumnsData({
|
|||
);
|
||||
}
|
||||
|
||||
case 'tagsV2': {
|
||||
let useDynamicOptions = getResolvedValue(column?.useDynamicOptions);
|
||||
if (useDynamicOptions) {
|
||||
const dynamicOptions = getResolvedValue(column?.dynamicOptions || [], { cellValue, rowData });
|
||||
columnOptions.selectOptions = Array.isArray(dynamicOptions) ? dynamicOptions : [];
|
||||
} else {
|
||||
const options = column?.options ?? [];
|
||||
columnOptions.selectOptions =
|
||||
options?.map((option) => ({
|
||||
label: option.label,
|
||||
value: option.value,
|
||||
optionColor: option.optionColor,
|
||||
labelColor: option.labelColor,
|
||||
})) ?? [];
|
||||
}
|
||||
|
||||
const tagsAutoAssignColors = getResolvedValue(column.autoAssignColors) ?? true;
|
||||
const sortTags = getResolvedValue(column.sortTags) ?? 'none';
|
||||
const allowMultipleSelection = getResolvedValue(column.allowMultipleSelection) ?? true;
|
||||
|
||||
return (
|
||||
<TagsV2Column
|
||||
options={columnOptions.selectOptions}
|
||||
value={cellValue}
|
||||
onChange={(value) => handleCellValueChange(row.index, column.key || column.name, value, row.original)}
|
||||
disabled={!isEditable}
|
||||
darkMode={darkMode}
|
||||
containerWidth={columnSize}
|
||||
defaultOptionsList={column?.defaultOptionsList || []}
|
||||
optionsLoadingState={
|
||||
useDynamicOptions && getResolvedValue(column?.optionsLoadingState) ? true : false
|
||||
}
|
||||
autoAssignColors={tagsAutoAssignColors}
|
||||
isEditable={isEditable}
|
||||
allowMultipleSelection={allowMultipleSelection}
|
||||
sortTags={sortTags}
|
||||
className="select-search table-select-search"
|
||||
column={column}
|
||||
isNewRow={columnForAddNewRow}
|
||||
horizontalAlignment={column?.horizontalAlignment}
|
||||
textColor={getResolvedValue(column.textColor, { cellValue, rowData })}
|
||||
id={id}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
case 'badge':
|
||||
case 'badges':
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -1,13 +1,23 @@
|
|||
import React from 'react';
|
||||
import _ from 'lodash';
|
||||
import Select, { components } from 'react-select';
|
||||
import CreatableSelect from 'react-select/creatable';
|
||||
import defaultStyles from './styles';
|
||||
const CustomInput = (props) => {
|
||||
return <components.Input {...props} data-cy={`${props.selectProps.dataCy || ''}-select-dropdown-input`} />;
|
||||
};
|
||||
export const SelectComponent = ({ options = [], value, onChange, closeMenuOnSelect, darkMode, ...restProps }) => {
|
||||
export const SelectComponent = ({
|
||||
options = [],
|
||||
value,
|
||||
onChange,
|
||||
closeMenuOnSelect,
|
||||
darkMode,
|
||||
creatable = false,
|
||||
...restProps
|
||||
}) => {
|
||||
const selectRef = React.useRef(null);
|
||||
const isDarkMode = darkMode ?? localStorage.getItem('darkMode') === 'true';
|
||||
const SelectElement = creatable ? CreatableSelect : Select;
|
||||
const {
|
||||
isMulti = false,
|
||||
styles = {},
|
||||
|
|
@ -54,7 +64,7 @@ export const SelectComponent = ({ options = [], value, onChange, closeMenuOnSele
|
|||
return option.label;
|
||||
};
|
||||
return (
|
||||
<Select
|
||||
<SelectElement
|
||||
{...restProps}
|
||||
ref={selectRef}
|
||||
selectRef={selectRef} // Exposed ref for custom components if needed
|
||||
|
|
|
|||
Loading…
Reference in a new issue