From 1a9143cb9e190aa844aa09a45af741c249208825 Mon Sep 17 00:00:00 2001 From: foxhoundn Date: Fri, 28 Jun 2024 10:50:38 +0200 Subject: [PATCH 1/5] Editable Tree --- package.json | 2 +- src/components/Tree/index.tsx | 40 +++++++++++++++++++++---------- src/stories/Tree/Tree.stories.tsx | 6 +++++ 3 files changed, 34 insertions(+), 14 deletions(-) diff --git a/package.json b/package.json index 3efa0ecd..07cecad4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@qoretechnologies/reqore", - "version": "0.46.9", + "version": "0.47.0", "description": "ReQore is a highly theme-able and modular UI library for React", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/src/components/Tree/index.tsx b/src/components/Tree/index.tsx index 15045293..315dc68f 100644 --- a/src/components/Tree/index.tsx +++ b/src/components/Tree/index.tsx @@ -23,6 +23,7 @@ export interface IReqoreTreeProps extends IReqorePanelProps, IWithReqoreSize { zoomable?: boolean; exportable?: boolean; defaultZoom?: 0 | 0.5 | 1 | 1.5 | 2; + editable?: boolean; } export interface ITreeStyle { @@ -53,7 +54,7 @@ export const ReqoreTree = ({ onItemClick, withLabelCopy, showControls = true, - + editable, exportable, zoomable, defaultZoom, @@ -91,24 +92,24 @@ export const ReqoreTree = ({ const isDeep = () => Object.keys(data).some((key: string): boolean => typeof data[key] === 'object'); - const renderTree = (data, k?: any, level = 1, path: string[] = []) => { - return Object.keys(data).map((key, index) => { - const dataType: string = getTypeFromValue(data[key]); + const renderTree = (_data, k?: any, level = 1, path: string[] = []) => { + return Object.keys(_data).map((key, index) => { + const dataType: string = getTypeFromValue(_data[key]); const displayKey: string = key; const stateKey = k ? `${k}_${key}` : key; - let isObject = typeof data[key] === 'object' && data[key] !== null; + let isObject = typeof _data[key] === 'object' && _data[key] !== null; let isExpandable = - typeof data[key] !== 'object' || + typeof _data[key] !== 'object' || items[stateKey] || (allExpanded && items[stateKey] !== false); - if (isObject && lodashSize(data[key]) === 0) { + if (isObject && lodashSize(_data[key]) === 0) { isObject = false; isExpandable = false; } - const badges: IReqoreButtonProps['badge'] = [`{...} ${lodashSize(data[key])} items`]; + const badges: IReqoreButtonProps['badge'] = [`{...} ${lodashSize(_data[key])} items`]; if (_showTypes) { badges.push({ @@ -126,7 +127,8 @@ export const ReqoreTree = ({ compact minimal className='reqore-tree-toggle' - icon={isExpandable ? 'ArrowDownSFill' : 'ArrowRightSFill'} + icon={'ArrowDownSFill'} + leftIconProps={{ rotation: isExpandable ? 0 : -90 }} intent={isExpandable ? 'info' : undefined} onClick={() => handleItemClick(stateKey, isExpandable)} flat={false} @@ -134,6 +136,18 @@ export const ReqoreTree = ({ > {displayKey} + {editable && ( + { + console.log(data, path, key, k, level); + }} + /> + )} ) : ( @@ -152,7 +166,7 @@ export const ReqoreTree = ({ size={getOneLessSize(zoomToSize[zoom])} onClick={() => { try { - navigator.clipboard.writeText(JSON.stringify(data[key])); + navigator.clipboard.writeText(JSON.stringify(_data[key])); addNotification({ content: 'Successfuly copied to clipboard', id: Date.now().toString(), @@ -167,16 +181,16 @@ export const ReqoreTree = ({ )} onItemClick?.(data[key], [...path, key])} + onClick={() => onItemClick?.(_data[key], [...path, key])} className='reqore-tree-label' size={zoomToSize[zoom]} > - {JSON.stringify(data[key])} + {JSON.stringify(_data[key])} )} {isExpandable && isObject - ? renderTree(data[key], stateKey, level + 1, [...path, key]) + ? renderTree(_data[key], stateKey, level + 1, [...path, key]) : null} ); diff --git a/src/stories/Tree/Tree.stories.tsx b/src/stories/Tree/Tree.stories.tsx index 71e4e44b..8fafc035 100644 --- a/src/stories/Tree/Tree.stories.tsx +++ b/src/stories/Tree/Tree.stories.tsx @@ -70,6 +70,12 @@ export const Exportable: Story = { }, }; +export const Editable: Story = { + args: { + editable: true, + }, +}; + export const Object: Story = { args: { exportable: true, From 94d88b28e9acf5cc62c711c2bd4527131da62aba Mon Sep 17 00:00:00 2001 From: foxhoundn Date: Fri, 28 Jun 2024 11:36:10 +0200 Subject: [PATCH 2/5] chore: Refactor Tree component to use compact delete button --- src/components/Tree/index.tsx | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/src/components/Tree/index.tsx b/src/components/Tree/index.tsx index 315dc68f..189cdde4 100644 --- a/src/components/Tree/index.tsx +++ b/src/components/Tree/index.tsx @@ -137,16 +137,7 @@ export const ReqoreTree = ({ {displayKey} {editable && ( - { - console.log(data, path, key, k, level); - }} - /> + )} ) : ( From 6539193e8a0131aed171127e73c03e65a0259c39 Mon Sep 17 00:00:00 2001 From: Foxhoundn Date: Mon, 1 Jul 2024 22:18:36 +0200 Subject: [PATCH 3/5] Editable tree progress --- __tests__/tree.test.tsx | 2 +- src/components/Drawer/index.tsx | 3 + src/components/Tree/index.tsx | 356 +++++++++++++++++++++++++++--- src/helpers/colors.ts | 6 +- src/helpers/utils.ts | 23 ++ src/mock/object.json | 76 +------ src/stories/Tree/Tree.stories.tsx | 75 ++++++- 7 files changed, 441 insertions(+), 100 deletions(-) diff --git a/__tests__/tree.test.tsx b/__tests__/tree.test.tsx index 53743549..5b26b79d 100644 --- a/__tests__/tree.test.tsx +++ b/__tests__/tree.test.tsx @@ -13,7 +13,7 @@ test('Renders basic properly', () => { ); - expect(document.querySelectorAll('.reqore-tree-toggle').length).toBe(7); + expect(document.querySelectorAll('.reqore-tree-toggle').length).toBe(8); }); test(' items can be expanded and collapsed', () => { diff --git a/src/components/Drawer/index.tsx b/src/components/Drawer/index.tsx index d087b802..e2be97ff 100644 --- a/src/components/Drawer/index.tsx +++ b/src/components/Drawer/index.tsx @@ -26,6 +26,7 @@ export interface IReqoreDrawerProps extends Omit void; hasBackdrop?: boolean; size?: string | 'auto'; + panelSize?: IReqorePanelProps['size']; maxSize?: string; minSize?: string; opacity?: number; @@ -157,6 +158,7 @@ export const ReqoreDrawer: React.FC = ({ height, actions = [], customZIndex, + panelSize, ...rest }: IReqoreDrawerProps) => { const animations = useReqoreProperty('animations'); @@ -337,6 +339,7 @@ export const ReqoreDrawer: React.FC = ({ {!_isHidden && ( | Array; + onDataChange?: (data: Record | Array) => void; mode?: 'tree' | 'copy'; expanded?: boolean; showTypes?: boolean; @@ -46,6 +57,104 @@ export const StyledTreeWrapper = styled.div` margin-left: ${({ level, size }) => (level ? level * GAP_FROM_SIZE[size] : 0)}px; `; +export interface IReqoreTreeManagementDialog extends IReqoreModalProps { + open?: boolean; + path?: string; + parentPath?: string; + type?: 'object' | 'array'; + parentType?: 'object' | 'array'; + data?: { key: string; value: any }; + onSave?: (data: { + key: string; + value: any; + originalData?: { key?: string; value?: any }; + }) => void; +} + +export const ReqoreTreeManagementDialog = ({ + path, + parentType, + type, + data, + onClose, + onSave, +}: IReqoreTreeManagementDialog) => { + const [key, setKey] = useState(data?.key); + const [value, setValue] = useState(data?.value); + + return ( + { + onSave({ + key, + value, + originalData: data, + }); + onClose(); + }, + }, + ]} + > + + {type === 'object' || (data?.key && parentType !== 'array') ? ( + + + setKey(e.target.value)} + placeholder='Key' + fluid + /> + + ) : null} + {typeof data?.value !== 'object' && ( + + + + setValue(e.target.value)} + placeholder='Value' + fluid + disabled={value === '[]' || value === '{}'} + /> + (value === '[]' ? setValue('') : setValue('[]'))} + intent={value === '[]' ? 'info' : undefined} + compact + textAlign='center' + > + [...] + + (value === '{}' ? setValue('') : setValue('{}'))} + intent={value === '{}' ? 'info' : undefined} + compact + textAlign='center' + label='{...}' + /> + + )} + + + ); +}; + export const ReqoreTree = ({ data, size = 'normal', @@ -58,6 +167,7 @@ export const ReqoreTree = ({ exportable, zoomable, defaultZoom, + onDataChange, ...rest }: IReqoreTreeProps) => { const [items, setItems] = useState({}); @@ -66,6 +176,9 @@ export const ReqoreTree = ({ const addNotification = useReqoreProperty('addNotification'); const [zoom, setZoom] = useState(defaultZoom || sizeToZoom[size]); const [showExportModal, setShowExportModal] = useState<'full' | 'current' | undefined>(undefined); + const [managementDialog, setManagementDialog] = useState({ + open: false, + }); const handleTypesClick = () => { setShowTypes(!_showTypes); @@ -96,20 +209,23 @@ export const ReqoreTree = ({ return Object.keys(_data).map((key, index) => { const dataType: string = getTypeFromValue(_data[key]); const displayKey: string = key; - const stateKey = k ? `${k}_${key}` : key; + const stateKey = k ? `${k}.${key}` : key; - let isObject = typeof _data[key] === 'object' && _data[key] !== null; - let isExpandable = + const isObject = typeof _data[key] === 'object' && _data[key] !== null; + const isExpandable = typeof _data[key] !== 'object' || items[stateKey] || (allExpanded && items[stateKey] !== false); - if (isObject && lodashSize(_data[key]) === 0) { - isObject = false; - isExpandable = false; - } - - const badges: IReqoreButtonProps['badge'] = [`{...} ${lodashSize(_data[key])} items`]; + const badges: IReqoreButtonProps['badge'] = [ + { + label: `${isArray(_data[key]) ? '[...]' : '{...}'} ${lodashSize(_data[key])} items`, + minimal: true, + labelEffect: { + weight: 'thin', + }, + }, + ]; if (_showTypes) { badges.push({ @@ -118,43 +234,132 @@ export const ReqoreTree = ({ }); } + const renderEditButton = () => { + if (!editable || (isArray(_data) && isObject)) { + return null; + } + + return ( + { + setManagementDialog({ + open: true, + parentPath: k, + path: stateKey, + parentType: isArray(_data) ? 'array' : 'object', + type: isArray(_data[key]) ? 'array' : 'object', + data: { key, value: _data[key] }, + }); + }} + /> + ); + }; + + const renderDeleteButton = () => { + if (!editable) { + return null; + } + + return ( + { + let modifiedData = cloneDeep(_data); + // Remove the item from the data + delete modifiedData[key]; + + if (isArray(modifiedData)) { + modifiedData = modifiedData.filter((item) => item); + } + // Update the data + const updatedData = k ? set(cloneDeep(data), k, modifiedData) : modifiedData; + + onDataChange?.(updatedData); + }} + /> + ); + }; + return ( {isObject ? ( - + {level !== 1 && } handleItemClick(stateKey, isExpandable)} - flat={false} badge={badges} > {displayKey} - {editable && ( - - )} + + {editable && isObject ? ( + + { + setManagementDialog({ + open: true, + path: stateKey, + parentType: isArray(_data) ? 'array' : 'object', + type: isArray(_data[key]) ? 'array' : 'object', + }); + }} + leftIconColor='info' + minimal + compact + /> + + ) : null} + {renderEditButton()} + {renderDeleteButton()} ) : ( {level !== 1 && } {displayKey}: + onItemClick?.(_data[key], [...path, key])} + className='reqore-tree-label' + size={zoomToSize[zoom]} + > + {JSON.stringify(_data[key])} + {withLabelCopy && ( - { try { navigator.clipboard.writeText(JSON.stringify(_data[key])); @@ -170,14 +375,8 @@ export const ReqoreTree = ({ }} /> )} - onItemClick?.(_data[key], [...path, key])} - className='reqore-tree-label' - size={zoomToSize[zoom]} - > - {JSON.stringify(_data[key])} - + {renderEditButton()} + {renderDeleteButton()} )} {isExpandable && isObject @@ -255,6 +454,78 @@ export const ReqoreTree = ({ {showExportModal && ( setShowExportModal(undefined)} /> )} + {managementDialog.open && ( + setManagementDialog({ open: false })} + onSave={({ key, value, originalData }) => { + const modifiedValue = + value === '{}' + ? {} + : value === '[]' + ? [] + : typeof value === 'object' + ? value + : parseInputValue(value); + + // If there is no path, it means we are adding a new item at the root level + if (!managementDialog.path) { + const updatedData = cloneDeep(data); + + if (originalData) { + unset(updatedData, originalData.key); + } else { + if (isArray(updatedData)) { + updatedData.push(modifiedValue); + } else { + updatedData[key] = modifiedValue; + } + } + + onDataChange?.(updatedData); + + setManagementDialog({ open: false }); + + return; + } + + const modifiedData = cloneDeep(get(data, managementDialog.path)); + + let updatedData; + + if (originalData) { + updatedData = set( + cloneDeep(data), + `${managementDialog.parentPath ? `${managementDialog.parentPath}.` : ''}${key}`, + modifiedValue + ); + + if (key !== originalData.key) { + unset( + updatedData, + `${managementDialog.parentPath ? `${managementDialog.parentPath}.` : ''}${ + originalData.key + }` + ); + } + } else { + // Add the item to the data + if (isArray(modifiedData)) { + modifiedData.push(modifiedValue); + } else { + modifiedData[key] = modifiedValue; + } + + // Update the data + updatedData = set(cloneDeep(data), managementDialog.path, modifiedData); + } + + onDataChange?.(updatedData); + + setManagementDialog({ open: false }); + }} + /> + )} - {renderTree(data, true)} + {renderTree(data)} + {editable && ( + + { + setManagementDialog({ + open: true, + path: '', + parentType: isArray(data) ? 'array' : 'object', + type: isArray(data) ? 'array' : 'object', + }); + }} + leftIconColor='info' + minimal + compact + /> + + )} ); diff --git a/src/helpers/colors.ts b/src/helpers/colors.ts index 8badd557..a5f81d0d 100644 --- a/src/helpers/colors.ts +++ b/src/helpers/colors.ts @@ -140,6 +140,10 @@ export const buildTheme = (theme: IReqoreTheme): IReqoreTheme => { // Add the original theme main color to the theme newTheme.originalMain = newTheme.main; + // Build the muted intent + const readableColor = getReadableColorFrom(newTheme.main, true); + newTheme.intents.muted = `${readableColor}30`; + if (!newTheme.notifications?.info) { newTheme.notifications.info = newTheme.intents.info || DEFAULT_INTENTS.info; } @@ -161,7 +165,7 @@ export const buildTheme = (theme: IReqoreTheme): IReqoreTheme => { } if (!newTheme.notifications?.muted) { - newTheme.notifications.muted = `${newTheme.intents.muted || DEFAULT_INTENTS.muted}30`; + newTheme.notifications.muted = newTheme.intents.muted || `${DEFAULT_INTENTS.muted}30`; } return newTheme; diff --git a/src/helpers/utils.ts b/src/helpers/utils.ts index 9d4243d4..287e5cc4 100644 --- a/src/helpers/utils.ts +++ b/src/helpers/utils.ts @@ -150,3 +150,26 @@ export const stringifyAndDecycleObject = (obj: any): string => { return value; }); }; + +export function parseInputValue(input) { + // Check for explicit quotes to save as a string + if (input.startsWith('"') && input.endsWith('"')) { + return input.slice(1, -1); // Remove the quotes + } + + // Attempt to parse as a number + const parsedNumber = Number(input); + if (!isNaN(parsedNumber)) { + return parsedNumber; + } + + // Attempt to parse as a boolean + if (input === 'true') { + return true; + } else if (input === 'false') { + return false; + } + + // Default to a string + return input; +} diff --git a/src/mock/object.json b/src/mock/object.json index 19a8a9cc..5020b506 100644 --- a/src/mock/object.json +++ b/src/mock/object.json @@ -18,15 +18,7 @@ "registered": "2015-03-19T05:00:22 -01:00", "latitude": -8.97737, "longitude": 110.576471, - "tags": [ - "aute", - "qui", - "ut", - "mollit", - "culpa", - "qui", - "irure" - ], + "tags": ["aute", "qui", "ut", "mollit", "culpa", "qui", "irure"], "friends": [ { "id": 0, @@ -63,15 +55,7 @@ "registered": "2014-11-25T11:58:45 -01:00", "latitude": 32.968241, "longitude": 21.905937, - "tags": [ - "occaecat", - "id", - "sit", - "eiusmod", - "tempor", - "adipisicing", - "sint" - ], + "tags": ["occaecat", "id", "sit", "eiusmod", "tempor", "adipisicing", "sint"], "friends": [ { "id": 0, @@ -108,15 +92,7 @@ "registered": "2019-06-05T08:58:50 -02:00", "latitude": -41.239498, "longitude": 11.237437, - "tags": [ - "ut", - "consectetur", - "enim", - "deserunt", - "anim", - "adipisicing", - "irure" - ], + "tags": ["ut", "consectetur", "enim", "deserunt", "anim", "adipisicing", "irure"], "friends": [ { "id": 0, @@ -153,15 +129,7 @@ "registered": "2020-02-24T06:26:21 -01:00", "latitude": -79.669108, "longitude": 19.666959, - "tags": [ - "magna", - "nisi", - "eiusmod", - "elit", - "eu", - "in", - "anim" - ], + "tags": ["magna", "nisi", "eiusmod", "elit", "eu", "in", "anim"], "friends": [ { "id": 0, @@ -198,15 +166,7 @@ "registered": "2018-05-30T10:39:01 -02:00", "latitude": 39.987328, "longitude": -18.121186, - "tags": [ - "amet", - "reprehenderit", - "elit", - "nostrud", - "sunt", - "consequat", - "fugiat" - ], + "tags": ["amet", "reprehenderit", "elit", "nostrud", "sunt", "consequat", "fugiat"], "friends": [ { "id": 0, @@ -243,15 +203,8 @@ "registered": "2015-09-29T09:39:54 -02:00", "latitude": -18.968669, "longitude": -33.485271, - "tags": [ - "mollit", - "officia", - "ipsum", - "veniam", - "laborum", - "laboris", - "consectetur" - ], + "emptyList": [], + "tags": ["mollit", "officia", "ipsum", "veniam", "laborum", "laboris", "consectetur"], "friends": [ { "id": 0, @@ -288,15 +241,7 @@ "registered": "2014-12-29T03:11:31 -01:00", "latitude": 55.032528, "longitude": 178.669012, - "tags": [ - "sit", - "non", - "et", - "labore", - "consectetur", - "voluptate", - "ea" - ], + "tags": ["sit", "non", "et", "labore", "consectetur", "voluptate", "ea"], "friends": [ { "id": 0, @@ -313,5 +258,6 @@ ], "greeting": "Hello, Levine Bray! You have 5 unread messages.", "favoriteFruit": "strawberry" - } -] \ No newline at end of file + }, + {} +] diff --git a/src/stories/Tree/Tree.stories.tsx b/src/stories/Tree/Tree.stories.tsx index 8fafc035..e4c22575 100644 --- a/src/stories/Tree/Tree.stories.tsx +++ b/src/stories/Tree/Tree.stories.tsx @@ -1,5 +1,6 @@ import { StoryObj } from '@storybook/react'; import { noop } from 'lodash'; +import { useState } from 'react'; import { IReqoreTreeProps, ReqoreTree } from '../../components/Tree'; import MockObject from '../../mock/object.json'; import { StoryMeta } from '../utils'; @@ -70,9 +71,81 @@ export const Exportable: Story = { }, }; -export const Editable: Story = { +export const EditableArray: Story = { + render: (args) => { + const [data, setData] = useState(args.data); + + return ( + { + setData(() => newData); + }} + /> + ); + }, + args: { + showTypes: false, + editable: true, + }, +}; + +export const EditableObject: Story = { + render: (args) => { + const [data, setData] = useState(args.data); + + return ( + { + setData(() => newData); + }} + /> + ); + }, args: { + showTypes: false, editable: true, + data: { + _id: '606d4c955f96372fd1b8bcd1', + index: 0, + guid: 'd08cde5f-e18e-4d7f-80a9-6541848ab830', + isActive: false, + balance: '$3,219.32', + picture: 'http://placehold.it/32x32', + age: 25, + eyeColor: 'brown', + name: 'Zelma Short', + gender: 'female', + company: 'LINGOAGE', + email: 'zelmashort@lingoage.com', + phone: '+1 (840) 429-2274', + address: '757 Woodside Avenue, Winchester, Marshall Islands, 2032', + about: + 'Ipsum est ex nisi veniam proident adipisicing. Occaecat Lorem minim amet aliqua laboris excepteur sint eu mollit laborum sunt. Duis aliquip nulla cillum labore culpa ullamco labore non in nostrud. Cupidatat nisi enim ullamco quis voluptate Lorem voluptate minim dolore esse irure eiusmod aliquip amet. Dolore laboris Lorem laboris irure magna sint dolor. In irure adipisicing minim ullamco commodo ad dolore elit occaecat dolor. Cillum commodo est commodo dolor enim velit.\r\n', + registered: '2015-03-19T05:00:22 -01:00', + latitude: -8.97737, + longitude: 110.576471, + tags: ['aute', 'qui', 'ut', 'mollit', 'culpa', 'qui', 'irure'], + friends: [ + { + id: 0, + name: 'Angel Gallagher', + }, + { + id: 1, + name: 'Rose Farmer', + }, + { + id: 2, + name: 'Jaclyn Keith', + }, + ], + greeting: 'Hello, Zelma Short! You have 5 unread messages.', + favoriteFruit: 'banana', + }, }, }; From b5dd09f0c93d6e89ed484c1f8f9ce5d6a567d59a Mon Sep 17 00:00:00 2001 From: foxhoundn Date: Tue, 2 Jul 2024 09:58:52 +0200 Subject: [PATCH 4/5] Updates and fixes --- src/components/Paragraph/index.tsx | 37 ++++++++++++---------- src/components/Tree/index.tsx | 51 +++++++++++++++++++++++++----- src/helpers/utils.ts | 4 +++ 3 files changed, 67 insertions(+), 25 deletions(-) diff --git a/src/components/Paragraph/index.tsx b/src/components/Paragraph/index.tsx index afa65776..0e692559 100644 --- a/src/components/Paragraph/index.tsx +++ b/src/components/Paragraph/index.tsx @@ -1,4 +1,4 @@ -import { memo } from 'react'; +import { forwardRef, memo } from 'react'; import styled from 'styled-components'; import { TEXT_FROM_SIZE, TSizes } from '../../constants/sizes'; import { isStringSize } from '../../helpers/utils'; @@ -23,21 +23,24 @@ export const StyledParagraph = styled(StyledTextEffect)` `; export const ReqoreP = memo( - ({ size, children, customTheme, intent, className, ...props }: IReqoreParagraphProps) => { - const theme = useReqoreTheme('main', customTheme, intent); + forwardRef( + ({ size, children, customTheme, intent, className, ...props }: IReqoreParagraphProps, ref) => { + const theme = useReqoreTheme('main', customTheme, intent); - return ( - - {children} - - ); - } + return ( + + {children} + + ); + } + ) ); diff --git a/src/components/Tree/index.tsx b/src/components/Tree/index.tsx index fc2e6ce5..5ad87774 100644 --- a/src/components/Tree/index.tsx +++ b/src/components/Tree/index.tsx @@ -1,4 +1,13 @@ -import { cloneDeep, get, isArray, size as lodashSize, set, unset } from 'lodash'; +import { + cloneDeep, + get, + isArray, + isBoolean, + isNumber, + size as lodashSize, + set, + unset, +} from 'lodash'; import { useMemo, useState } from 'react'; import styled from 'styled-components'; import { @@ -7,6 +16,7 @@ import { ReqoreModal, ReqoreP, ReqorePanel, + ReqorePopover, ReqoreTag, ReqoreTextarea, useReqoreProperty, @@ -85,7 +95,7 @@ export const ReqoreTreeManagementDialog = ({ return ( @@ -338,17 +356,34 @@ export const ReqoreTree = ({ ) : ( {level !== 1 && } - {displayKey}: - + onItemClick?.(_data[key], [...path, key])} className='reqore-tree-label' + customTheme={{ + text: { + color: isBoolean(_data[key]) + ? _data[key] + ? 'success:lighten:5' + : 'danger:lighten:10' + : isNumber(_data[key]) + ? 'info:lighten:10' + : undefined, + }, + }} size={zoomToSize[zoom]} > {JSON.stringify(_data[key])} diff --git a/src/helpers/utils.ts b/src/helpers/utils.ts index 287e5cc4..089823a8 100644 --- a/src/helpers/utils.ts +++ b/src/helpers/utils.ts @@ -152,6 +152,10 @@ export const stringifyAndDecycleObject = (obj: any): string => { }; export function parseInputValue(input) { + if (isBoolean(input)) { + return input; + } + // Check for explicit quotes to save as a string if (input.startsWith('"') && input.endsWith('"')) { return input.slice(1, -1); // Remove the quotes From a368541c625b6f4ffcad62ca401c4b94a571b3e5 Mon Sep 17 00:00:00 2001 From: foxhoundn Date: Tue, 2 Jul 2024 12:49:49 +0200 Subject: [PATCH 5/5] Editable Tree tests --- src/components/Tree/index.tsx | 115 +++--------------------------- src/components/Tree/modal.tsx | 108 ++++++++++++++++++++++++++++ src/stories/Tree/Tree.stories.tsx | 81 +++++++++++++++++++++ 3 files changed, 200 insertions(+), 104 deletions(-) create mode 100644 src/components/Tree/modal.tsx diff --git a/src/components/Tree/index.tsx b/src/components/Tree/index.tsx index 5ad87774..475a4f57 100644 --- a/src/components/Tree/index.tsx +++ b/src/components/Tree/index.tsx @@ -12,13 +12,9 @@ import { useMemo, useState } from 'react'; import styled from 'styled-components'; import { ReqoreHorizontalSpacer, - ReqoreInput, - ReqoreModal, ReqoreP, ReqorePanel, ReqorePopover, - ReqoreTag, - ReqoreTextarea, useReqoreProperty, } from '../..'; import { GAP_FROM_SIZE, TSizes } from '../../constants/sizes'; @@ -28,9 +24,9 @@ import { IWithReqoreSize } from '../../types/global'; import ReqoreButton, { IReqoreButtonProps } from '../Button'; import ReqoreControlGroup from '../ControlGroup'; import { ReqoreExportModal } from '../ExportModal'; -import { IReqoreModalProps } from '../Modal'; import { IReqorePanelAction, IReqorePanelProps } from '../Panel'; import { getExportActions, getZoomActions, sizeToZoom, zoomToSize } from '../Table/helpers'; +import { IReqoreTreeManagementDialog, ReqoreTreeManagementDialog } from './modal'; export interface IReqoreTreeProps extends IReqorePanelProps, IWithReqoreSize { data: Record | Array; @@ -67,104 +63,6 @@ export const StyledTreeWrapper = styled.div` margin-left: ${({ level, size }) => (level ? level * GAP_FROM_SIZE[size] : 0)}px; `; -export interface IReqoreTreeManagementDialog extends IReqoreModalProps { - open?: boolean; - path?: string; - parentPath?: string; - type?: 'object' | 'array'; - parentType?: 'object' | 'array'; - data?: { key: string; value: any }; - onSave?: (data: { - key: string; - value: any; - originalData?: { key?: string; value?: any }; - }) => void; -} - -export const ReqoreTreeManagementDialog = ({ - path, - parentType, - type, - data, - onClose, - onSave, -}: IReqoreTreeManagementDialog) => { - const [key, setKey] = useState(data?.key); - const [value, setValue] = useState(data?.value); - - return ( - { - onSave({ - key, - value, - originalData: data, - }); - onClose(); - }, - }, - ]} - > - - {type === 'object' || (data?.key && parentType !== 'array') ? ( - - - setKey(e.target.value)} - placeholder='Key' - fluid - /> - - ) : null} - {typeof data?.value !== 'object' && ( - - - - setValue(e.target.value)} - placeholder='Value' - fluid - disabled={value === '[]' || value === '{}'} - /> - (value === '[]' ? setValue('') : setValue('[]'))} - intent={value === '[]' ? 'info' : undefined} - compact - textAlign='center' - > - [...] - - (value === '{}' ? setValue('') : setValue('{}'))} - intent={value === '{}' ? 'info' : undefined} - compact - textAlign='center' - label='{...}' - /> - - )} - - - ); -}; - export const ReqoreTree = ({ data, size = 'normal', @@ -253,6 +151,7 @@ export const ReqoreTree = ({ + {isObject ? ( {level !== 1 && } @@ -576,12 +481,14 @@ export const ReqoreTree = ({ actions={actions} > {renderTree(data)} + {editable && ( { setManagementDialog({ diff --git a/src/components/Tree/modal.tsx b/src/components/Tree/modal.tsx new file mode 100644 index 00000000..470cedd3 --- /dev/null +++ b/src/components/Tree/modal.tsx @@ -0,0 +1,108 @@ +import { useState } from 'react'; +import { ReqoreTextarea } from '../..'; +import ReqoreButton from '../Button'; +import ReqoreControlGroup from '../ControlGroup'; +import ReqoreInput from '../Input'; +import { IReqoreModalProps, ReqoreModal } from '../Modal'; +import ReqoreTag from '../Tag'; + +export interface IReqoreTreeManagementDialog extends IReqoreModalProps { + open?: boolean; + path?: string; + parentPath?: string; + type?: 'object' | 'array'; + parentType?: 'object' | 'array'; + data?: { key: string; value: any }; + onSave?: (data: { + key: string; + value: any; + originalData?: { key?: string; value?: any }; + }) => void; +} + +export const ReqoreTreeManagementDialog = ({ + path, + parentType, + type, + data, + onClose, + onSave, +}: IReqoreTreeManagementDialog) => { + const [key, setKey] = useState(data?.key); + const [value, setValue] = useState(data?.value); + + return ( + { + onSave({ + key, + value, + originalData: data, + }); + onClose(); + }, + }, + ]} + > + + {type === 'object' || (data?.key && parentType !== 'array') ? ( + + + setKey(e.target.value)} + placeholder='Key' + fluid + /> + + ) : null} + {typeof data?.value !== 'object' && ( + + + + setValue(e.target.value)} + placeholder='Value' + fluid + disabled={value === '[]' || value === '{}'} + /> + (value === '[]' ? setValue('') : setValue('[]'))} + intent={value === '[]' ? 'info' : undefined} + compact + textAlign='center' + > + [...] + + (value === '{}' ? setValue('') : setValue('{}'))} + intent={value === '{}' ? 'info' : undefined} + compact + textAlign='center' + label='{...}' + /> + + )} + + + ); +}; diff --git a/src/stories/Tree/Tree.stories.tsx b/src/stories/Tree/Tree.stories.tsx index e4c22575..0d7d2a44 100644 --- a/src/stories/Tree/Tree.stories.tsx +++ b/src/stories/Tree/Tree.stories.tsx @@ -1,6 +1,9 @@ +import { expect } from '@storybook/jest'; import { StoryObj } from '@storybook/react'; +import { fireEvent } from '@storybook/testing-library'; import { noop } from 'lodash'; import { useState } from 'react'; +import { _testsClickButton, _testsWaitForText } from '../../../__tests__/utils'; import { IReqoreTreeProps, ReqoreTree } from '../../components/Tree'; import MockObject from '../../mock/object.json'; import { StoryMeta } from '../utils'; @@ -201,3 +204,81 @@ export const WithDefaultZoom: Story = { defaultZoom: 2, }, }; + +export const NewArrayItemCanBeAdded: Story = { + ...EditableArray, + play: async () => { + await expect(document.querySelectorAll('.reqore-tree-item').length).toBe(8); + await _testsClickButton({ selector: '.reqore-tree-add' }); + await _testsWaitForText('Adding new item'); + await expect(document.querySelector('.reqore-tree-save')).toBeDisabled(); + await fireEvent.change(document.querySelector('.reqore-textarea'), { + target: { value: 'New item' }, + }); + await expect(document.querySelector('.reqore-tree-save')).toBeEnabled(); + await _testsClickButton({ selector: '.reqore-tree-save' }); + await expect(document.querySelectorAll('.reqore-tree-item').length).toBe(9); + await _testsWaitForText('"New item"'); + }, +}; + +export const NewArrayItemCanBeEdited: Story = { + ...NewArrayItemCanBeAdded, + play: async (args) => { + await NewArrayItemCanBeAdded.play(args); + await _testsClickButton({ selector: '.reqore-tree-edit' }); + await fireEvent.change(document.querySelectorAll('.reqore-textarea')[0], { + target: { value: 'New item edited' }, + }); + await expect(document.querySelector('.reqore-tree-save')).toBeEnabled(); + await _testsClickButton({ selector: '.reqore-tree-save' }); + await expect(document.querySelectorAll('.reqore-tree-item').length).toBe(9); + await _testsWaitForText('"New item edited"'); + }, +}; + +export const NewObjectItemCanBeAdded: Story = { + ...EditableObject, + play: async () => { + await expect(document.querySelectorAll('.reqore-tree-item').length).toBe(22); + await _testsClickButton({ selector: '.reqore-tree-add' }); + await _testsWaitForText('Adding new item'); + await expect(document.querySelector('.reqore-tree-save')).toBeDisabled(); + await fireEvent.change(document.querySelector('.reqore-input'), { + target: { value: 'New item' }, + }); + await fireEvent.change(document.querySelectorAll('.reqore-textarea')[0], { + target: { value: 'New item value' }, + }); + await expect(document.querySelector('.reqore-tree-save')).toBeEnabled(); + await _testsClickButton({ selector: '.reqore-tree-save' }); + await expect(document.querySelectorAll('.reqore-tree-item').length).toBe(23); + await _testsWaitForText('New item:'); + await _testsWaitForText('"New item value"'); + }, +}; + +export const ObjectItemCanBeEdited: Story = { + ...EditableObject, + play: async () => { + await _testsClickButton({ selector: '.reqore-tree-edit', nth: 19 }); + await fireEvent.change(document.querySelectorAll('.reqore-input')[0], { + target: { value: 'updated item key' }, + }); + await expect(document.querySelector('.reqore-tree-save')).toBeEnabled(); + await _testsClickButton({ selector: '.reqore-tree-save' }); + await expect(document.querySelectorAll('.reqore-tree-item').length).toBe(22); + await _testsWaitForText('updated item key'); + await _testsClickButton({ label: 'updated item key' }); + await _testsClickButton({ label: '1' }); + }, +}; + +export const ItemsCanBeDeleted: Story = { + ...EditableObject, + play: async () => { + await _testsClickButton({ selector: '.reqore-tree-delete', nth: 4 }); + await _testsClickButton({ selector: '.reqore-tree-delete', nth: 18 }); + await expect(document.querySelectorAll('.reqore-tree-item').length).toBe(20); + }, +};