From 63d98282d068ca89fd1dabfb0c118cdbf64cc469 Mon Sep 17 00:00:00 2001 From: walter Van Date: Wed, 22 Sep 2021 16:14:11 +1000 Subject: [PATCH 01/10] initial commit --- web/src/components/TagPicker.tsx | 57 +++++++++++++++++++ .../components/cardDetails/CardDetails.tsx | 4 ++ 2 files changed, 61 insertions(+) create mode 100644 web/src/components/TagPicker.tsx diff --git a/web/src/components/TagPicker.tsx b/web/src/components/TagPicker.tsx new file mode 100644 index 00000000..b8d8b2ba --- /dev/null +++ b/web/src/components/TagPicker.tsx @@ -0,0 +1,57 @@ +import React, { Requireable, useRef, useState } from 'react'; +import PropTypes from 'prop-types'; +import { Callout, DefaultButton, Dropdown, IStackItemProps, List, Stack, Text } from '@fluentui/react'; +import { ITag } from '../controllers/Tag'; +import { ICard } from '../controllers/Card'; +import { useApp } from '../AppContext'; +import { Tag } from './Tag'; + +export interface ITagPickerProps { + targetCard?: ICard; +} + +export const TagPicker: React.VoidFunctionComponent = ({ targetCard }) => { + const [pickerActive, setPickerActive] = useState(false); + const calloutTarget = useRef(null); + + // need to fetch tags from card to display + console.log(targetCard); + + // user variable is used to access the tags available to the user + const user = useApp(); + const tags = user.user?.tags; + return ( + + + Tags + + +
+ + {/* inject code to display card's tags here */} + Card tags exist here + +
+
+ setPickerActive(old => !old)} /> + {pickerActive ? ( + setPickerActive(false)} + > + + Would you look at that callout! + + + ) : null} +
+ ); +}; + +TagPicker.propTypes = { + targetCard: (PropTypes.object as Requireable) +}; + +TagPicker.defaultProps = { + targetCard: undefined, +}; diff --git a/web/src/components/cardDetails/CardDetails.tsx b/web/src/components/cardDetails/CardDetails.tsx index 1fad2733..dbf51830 100644 --- a/web/src/components/cardDetails/CardDetails.tsx +++ b/web/src/components/cardDetails/CardDetails.tsx @@ -8,6 +8,7 @@ import { CardExtraField } from './CardExtraField'; import { CardImageField } from './CardImageField'; import { CardMandatoryField } from './CardMandatoryField'; import { CardNoteField } from './CardNoteField'; +import { TagPicker } from '../TagPicker'; export interface ICardDetailsProps { card: ICard; @@ -69,6 +70,9 @@ export const CardDetails: React.VoidFunctionComponent = ({ ed + + + {mFields.map((field) => ( From 74d7d7c5231c75604cd32c99a873851609e06216 Mon Sep 17 00:00:00 2001 From: walter Van Date: Wed, 22 Sep 2021 21:14:16 +1000 Subject: [PATCH 02/10] prototyped TagPicker and TagEditor: to accompany callout flow --- package-lock.json | 31 +++++---- package.json | 5 +- web/src/components/TagEditor.tsx | 37 ++++++++++ web/src/components/TagPicker.tsx | 114 ++++++++++++++++++++----------- 4 files changed, 131 insertions(+), 56 deletions(-) create mode 100644 web/src/components/TagEditor.tsx diff --git a/package-lock.json b/package-lock.json index b7629eb1..e7dc45a0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,9 @@ "shared", "web" ], + "dependencies": { + "@fluentui/react-hooks": "^8.3.2" + }, "devDependencies": { "node-git-hooks": "^1.0.6" }, @@ -814,13 +817,13 @@ } }, "node_modules/@fluentui/react-hooks": { - "version": "8.3.1", - "resolved": "https://registry.npmjs.org/@fluentui/react-hooks/-/react-hooks-8.3.1.tgz", - "integrity": "sha512-fgkL4/4m8ds7dK6+o6qfk9Ok1ssbTV3dA7k1w5xZgw/FE4AWTt6zfLx9HhGpxF71l0X+R0DCWWb6W/LqE7+Ylg==", + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/@fluentui/react-hooks/-/react-hooks-8.3.2.tgz", + "integrity": "sha512-mGmDCaUjavYj4Bv/IPoNix4HMXX2ZPnPMfyH5X0QjiFEeSuOFlIM6By0sV6Vf30dsjoHNdUUsU461axtFTRVsg==", "dependencies": { "@fluentui/react-window-provider": "^2.1.4", "@fluentui/set-version": "^8.1.4", - "@fluentui/utilities": "^8.3.1", + "@fluentui/utilities": "^8.3.2", "tslib": "^2.1.0" }, "peerDependencies": { @@ -878,9 +881,9 @@ } }, "node_modules/@fluentui/utilities": { - "version": "8.3.1", - "resolved": "https://registry.npmjs.org/@fluentui/utilities/-/utilities-8.3.1.tgz", - "integrity": "sha512-V/6bokboB7J1di6XWnS2AuT1A2x0N8BvfCbdaTqvrmCarmViaY/3cawO8shV91d+ahiR2ZzN0CqOMkIDvjr4tA==", + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/@fluentui/utilities/-/utilities-8.3.2.tgz", + "integrity": "sha512-XP/NG3jg8LqLzU139SuNzO01nu7IAizlnC9KWNkYxrV5Hc9pGrW/seKloc7F7RVK36Vr5l4gl3DXX9lhUZUP/Q==", "dependencies": { "@fluentui/dom-utilities": "^2.1.4", "@fluentui/merge-styles": "^8.1.5", @@ -10338,13 +10341,13 @@ } }, "@fluentui/react-hooks": { - "version": "8.3.1", - "resolved": "https://registry.npmjs.org/@fluentui/react-hooks/-/react-hooks-8.3.1.tgz", - "integrity": "sha512-fgkL4/4m8ds7dK6+o6qfk9Ok1ssbTV3dA7k1w5xZgw/FE4AWTt6zfLx9HhGpxF71l0X+R0DCWWb6W/LqE7+Ylg==", + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/@fluentui/react-hooks/-/react-hooks-8.3.2.tgz", + "integrity": "sha512-mGmDCaUjavYj4Bv/IPoNix4HMXX2ZPnPMfyH5X0QjiFEeSuOFlIM6By0sV6Vf30dsjoHNdUUsU461axtFTRVsg==", "requires": { "@fluentui/react-window-provider": "^2.1.4", "@fluentui/set-version": "^8.1.4", - "@fluentui/utilities": "^8.3.1", + "@fluentui/utilities": "^8.3.2", "tslib": "^2.1.0" } }, @@ -10390,9 +10393,9 @@ } }, "@fluentui/utilities": { - "version": "8.3.1", - "resolved": "https://registry.npmjs.org/@fluentui/utilities/-/utilities-8.3.1.tgz", - "integrity": "sha512-V/6bokboB7J1di6XWnS2AuT1A2x0N8BvfCbdaTqvrmCarmViaY/3cawO8shV91d+ahiR2ZzN0CqOMkIDvjr4tA==", + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/@fluentui/utilities/-/utilities-8.3.2.tgz", + "integrity": "sha512-XP/NG3jg8LqLzU139SuNzO01nu7IAizlnC9KWNkYxrV5Hc9pGrW/seKloc7F7RVK36Vr5l4gl3DXX9lhUZUP/Q==", "requires": { "@fluentui/dom-utilities": "^2.1.4", "@fluentui/merge-styles": "^8.1.5", diff --git a/package.json b/package.json index 677fcd92..e5a7ae91 100644 --- a/package.json +++ b/package.json @@ -23,5 +23,8 @@ "server", "shared", "web" - ] + ], + "dependencies": { + "@fluentui/react-hooks": "^8.3.2" + } } diff --git a/web/src/components/TagEditor.tsx b/web/src/components/TagEditor.tsx new file mode 100644 index 00000000..f34d6db1 --- /dev/null +++ b/web/src/components/TagEditor.tsx @@ -0,0 +1,37 @@ +import { + Callout, ICalloutProps, +} from '@fluentui/react'; +import PropTypes, { Requireable } from 'prop-types'; +import React from 'react'; +import { ITag } from '../controllers/Tag'; + +// closing Function is called to dismiss callout +export interface ITagEditorProps { + tag: ITag; + anchor: ICalloutProps['target']; + closingFunction?: () => void; +} + +export const TagEditor: React.VoidFunctionComponent = ({ + tag, anchor, closingFunction, +}) => ( + { if (closingFunction) closingFunction(); }} + > + + Calling edit for + {tag.id} + + +); + +TagEditor.propTypes = { + tag: (PropTypes.object as Requireable).isRequired, + anchor: (PropTypes.object as Requireable).isRequired, + closingFunction: PropTypes.func, +}; + +TagEditor.defaultProps = { + closingFunction: () => { }, +}; diff --git a/web/src/components/TagPicker.tsx b/web/src/components/TagPicker.tsx index b8d8b2ba..a8b2a074 100644 --- a/web/src/components/TagPicker.tsx +++ b/web/src/components/TagPicker.tsx @@ -1,57 +1,89 @@ -import React, { Requireable, useRef, useState } from 'react'; +import React, { + Requireable, useRef, useState, +} from 'react'; import PropTypes from 'prop-types'; -import { Callout, DefaultButton, Dropdown, IStackItemProps, List, Stack, Text } from '@fluentui/react'; -import { ITag } from '../controllers/Tag'; +import { + Callout, DefaultButton, Stack, TextField, +} from '@fluentui/react'; import { ICard } from '../controllers/Card'; import { useApp } from '../AppContext'; import { Tag } from './Tag'; +import { ITagEditorProps, TagEditor } from './TagEditor'; export interface ITagPickerProps { - targetCard?: ICard; + targetCard: ICard; } export const TagPicker: React.VoidFunctionComponent = ({ targetCard }) => { - const [pickerActive, setPickerActive] = useState(false); - const calloutTarget = useRef(null); + const user = useApp(); + const [pickerActive, setPickerActive] = useState(false); + const [tagSearchString, setTagSearchString] = useState(''); + const [focusedTag, setFocusedTag] = useState(); + const calloutTarget = useRef(null); - // need to fetch tags from card to display - console.log(targetCard); - - // user variable is used to access the tags available to the user - const user = useApp(); - const tags = user.user?.tags; - return ( + // user variable is used to access the tags available to the user + return ( + + + Tags + + + {/* could style to fit measurements */} - - Tags - - -
- - {/* inject code to display card's tags here */} - Card tags exist here - -
-
- setPickerActive(old => !old)} /> - {pickerActive ? ( - setPickerActive(false)} - > - - Would you look at that callout! - - - ) : null} + {targetCard?.tags.map((t) => { }} />)}
- ); -}; -TagPicker.propTypes = { - targetCard: (PropTypes.object as Requireable) +
+
+ setPickerActive((old) => !old)} /> +
+ {pickerActive + ? ( + setPickerActive(false)} + > + + + setTagSearchString(event.currentTarget.value)} + /> + + + {/* Tags available to use are listed here */} + {user.user?.tags + .filter((t) => t.label.toUpperCase().startsWith(tagSearchString.toUpperCase())) + .filter((t) => !targetCard?.tags.includes(t)) + .map((t) => ( + + + setFocusedTag({ + tag: t, + anchor: event.currentTarget as Element, + })} + /> + + ))} + + {focusedTag + && ( + setFocusedTag(undefined)} + /> + )} + + ) + : null} +
+ ); }; -TagPicker.defaultProps = { - targetCard: undefined, +TagPicker.propTypes = { + targetCard: (PropTypes.object as Requireable).isRequired, }; From 2a82ddc9e1d9263ac74d04dc4c6e99489366d7e0 Mon Sep 17 00:00:00 2001 From: walter Van Date: Thu, 23 Sep 2021 14:16:02 +1000 Subject: [PATCH 03/10] linked addTag context callback. Preferred array of refs over useId See https://github.com/chomosuke/IT-PROJECT-PorkBellyPro/issues/84#issuecomment-925492480 --- package-lock.json | 5 ++- package.json | 5 +-- web/package.json | 9 +++--- web/src/components/TagPicker.tsx | 55 +++++++++++++++++++------------- 4 files changed, 41 insertions(+), 33 deletions(-) diff --git a/package-lock.json b/package-lock.json index e7dc45a0..9c55b81c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,9 +11,6 @@ "shared", "web" ], - "dependencies": { - "@fluentui/react-hooks": "^8.3.2" - }, "devDependencies": { "node-git-hooks": "^1.0.6" }, @@ -9652,6 +9649,7 @@ "name": "@porkbellypro/crm-web", "dependencies": { "@fluentui/react": "^8.28.2", + "@fluentui/react-hooks": "^8.3.2", "@porkbellypro/crm-shared": "file:shared", "@tsconfig/recommended": "^1.0.1", "@types/history": "^4.7.9", @@ -11118,6 +11116,7 @@ "version": "file:web", "requires": { "@fluentui/react": "^8.28.2", + "@fluentui/react-hooks": "^8.3.2", "@porkbellypro/crm-shared": "file:shared", "@tsconfig/recommended": "^1.0.1", "@types/history": "^4.7.9", diff --git a/package.json b/package.json index e5a7ae91..677fcd92 100644 --- a/package.json +++ b/package.json @@ -23,8 +23,5 @@ "server", "shared", "web" - ], - "dependencies": { - "@fluentui/react-hooks": "^8.3.2" - } + ] } diff --git a/web/package.json b/web/package.json index 552b0af5..e1d475dc 100644 --- a/web/package.json +++ b/web/package.json @@ -1,6 +1,6 @@ { "name": "@porkbellypro/crm-web", - "bin":{ + "bin": { "web-build": "./build.mjs" }, "scripts": { @@ -10,6 +10,7 @@ }, "dependencies": { "@fluentui/react": "^8.28.2", + "@fluentui/react-hooks": "^8.3.2", "@porkbellypro/crm-shared": "file:shared", "@tsconfig/recommended": "^1.0.1", "@types/history": "^4.7.9", @@ -35,13 +36,13 @@ "@types/jest": "^27.0.1", "@typescript-eslint/eslint-plugin": "^4.29.1", "@typescript-eslint/parser": "^4.29.1", + "eslint": "^7.32.0", "eslint-config-airbnb-typescript": "^12.3.1", "eslint-plugin-import": "^2.24.0", "eslint-plugin-jsx-a11y": "^6.4.1", "eslint-plugin-react": "^7.24.0", "eslint-plugin-react-hooks": "^4.2.0", - "eslint": "^7.32.0", - "ts-jest": "^27.0.5", - "jest": "^27.0.6" + "jest": "^27.0.6", + "ts-jest": "^27.0.5" } } diff --git a/web/src/components/TagPicker.tsx b/web/src/components/TagPicker.tsx index a8b2a074..163ed97b 100644 --- a/web/src/components/TagPicker.tsx +++ b/web/src/components/TagPicker.tsx @@ -5,10 +5,12 @@ import PropTypes from 'prop-types'; import { Callout, DefaultButton, Stack, TextField, } from '@fluentui/react'; +import { useId } from '@fluentui/react-hooks'; import { ICard } from '../controllers/Card'; import { useApp } from '../AppContext'; import { Tag } from './Tag'; import { ITagEditorProps, TagEditor } from './TagEditor'; +import { ITagProperties } from '../controllers/Tag'; export interface ITagPickerProps { targetCard: ICard; @@ -19,7 +21,16 @@ export const TagPicker: React.VoidFunctionComponent = ({ target const [pickerActive, setPickerActive] = useState(false); const [tagSearchString, setTagSearchString] = useState(''); const [focusedTag, setFocusedTag] = useState(); - const calloutTarget = useRef(null); + const pickerTargetId = useId('picker-target'); + // map stores tag id keys and element values + const tagTargetRefs = useRef(new Map()); + + function getNewTag(): void { + const tagProps: Partial = { + label: tagSearchString || 'New Tag', + }; + user.newTag(tagProps); + } // user variable is used to access the tags available to the user return ( @@ -27,20 +38,18 @@ export const TagPicker: React.VoidFunctionComponent = ({ target Tags - + {/* could style to fit measurements */} {targetCard?.tags.map((t) => { }} />)} -
- setPickerActive((old) => !old)} /> -
+ setPickerActive((old) => !old)} /> {pickerActive ? ( setPickerActive(false)} > @@ -50,32 +59,34 @@ export const TagPicker: React.VoidFunctionComponent = ({ target value={tagSearchString} onChange={(event) => setTagSearchString(event.currentTarget.value)} /> - +
{/* Tags available to use are listed here */} {user.user?.tags .filter((t) => t.label.toUpperCase().startsWith(tagSearchString.toUpperCase())) .filter((t) => !targetCard?.tags.includes(t)) .map((t) => ( - - - setFocusedTag({ - tag: t, - anchor: event.currentTarget as Element, - })} - /> - +
tagTargetRefs.current.set(t.id, el)}> + + + setFocusedTag({ + tag: t, + anchor: tagTargetRefs.current.get(t.id), + })} + /> + +
))} {focusedTag && ( - setFocusedTag(undefined)} - /> + setFocusedTag(undefined)} + /> )} ) From 608a4dd1d0a4820b6549dd79f045de678b2fec1b Mon Sep 17 00:00:00 2001 From: walter Van Date: Thu, 23 Sep 2021 18:46:56 +1000 Subject: [PATCH 04/10] Implemented TagWrapper, Tag Label and colour editor --- package-lock.json | 2 + web/package.json | 1 + web/src/components/TagEditor.tsx | 99 ++++++++++++++++--- web/src/components/TagPicker.tsx | 56 +++++------ web/src/components/TagWrapper.tsx | 54 ++++++++++ .../components/cardDetails/CardDetails.tsx | 2 +- 6 files changed, 168 insertions(+), 46 deletions(-) create mode 100644 web/src/components/TagWrapper.tsx diff --git a/package-lock.json b/package-lock.json index 9c55b81c..afa6705c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9659,6 +9659,7 @@ "@types/react-test-renderer": "^17.0.1", "argparse": "^2.0.1", "buffer": "^6.0.3", + "color-string": "^1.6.0", "history": "^5.0.1", "html-webpack-plugin": "^5.3.2", "jimp": "^0.16.1", @@ -11129,6 +11130,7 @@ "@typescript-eslint/parser": "^4.29.1", "argparse": "^2.0.1", "buffer": "^6.0.3", + "color-string": "^1.6.0", "eslint": "^7.32.0", "eslint-config-airbnb-typescript": "^12.3.1", "eslint-plugin-import": "^2.24.0", diff --git a/web/package.json b/web/package.json index e1d475dc..c49f9681 100644 --- a/web/package.json +++ b/web/package.json @@ -20,6 +20,7 @@ "@types/react-test-renderer": "^17.0.1", "argparse": "^2.0.1", "buffer": "^6.0.3", + "color-string": "^1.6.0", "history": "^5.0.1", "html-webpack-plugin": "^5.3.2", "jimp": "^0.16.1", diff --git a/web/src/components/TagEditor.tsx b/web/src/components/TagEditor.tsx index f34d6db1..40cfa365 100644 --- a/web/src/components/TagEditor.tsx +++ b/web/src/components/TagEditor.tsx @@ -1,9 +1,9 @@ import { - Callout, ICalloutProps, + Callout, DefaultButton, ICalloutProps, Stack, TextField, mergeStyles, } from '@fluentui/react'; import PropTypes, { Requireable } from 'prop-types'; -import React from 'react'; -import { ITag } from '../controllers/Tag'; +import React, { useState } from 'react'; +import { ITag, ITagProperties } from '../controllers/Tag'; // closing Function is called to dismiss callout export interface ITagEditorProps { @@ -12,23 +12,92 @@ export interface ITagEditorProps { closingFunction?: () => void; } +const getSwatchClassNames = (selectedColour: string) => { + const height = 24; + const borderRadius = height / 2; + const width = 24; + // Extracted from Figma + const colours = [ + '#BF7829', + '#127976', + '#D61317', + '#F3B27A', + '#E5E5E5', + '#77A69E', + '#BC8282', + '#78AFB2', + '#84A9C3', + '#5D68A6', + '#662525', + '#FBE900', + ]; + + return colours.map((colour) => [colour, + mergeStyles([{ + height, + width, + borderRadius, + backgroundColor: colour, + }, + // when selectedColour matches then we have feedback on what colour is selected + (colour === selectedColour) && { + border: '2px solid black', + boxSizing: 'border-box', + }])]); +}; + export const TagEditor: React.VoidFunctionComponent = ({ tag, anchor, closingFunction, -}) => ( - { if (closingFunction) closingFunction(); }} - > - - Calling edit for - {tag.id} - - -); +}) => { + const [unstagedState, setState] = useState(tag); + const updateLabel = (newLabel: string) => setState({ ...unstagedState, label: newLabel }); + const updateColor = (newColor: string) => setState({ ...unstagedState, color: newColor }); + + const deleteTag = async () => { + tag.delete(); + if (closingFunction) closingFunction(); + }; + + const swatchStyles = getSwatchClassNames(unstagedState.color); + + return ( + { + tag.commit(unstagedState); + if (closingFunction) closingFunction(); + }} + > + updateLabel(ev.currentTarget.value)} + /> + + {/* Can replace with Diaglog */} + + {/* color swatches */} + + {swatchStyles.map(([color, className]) => ( + // Colour swatches +
updateColor(color)} + onKeyDown={undefined} + /> + ))} + + + ); +}; TagEditor.propTypes = { tag: (PropTypes.object as Requireable).isRequired, - anchor: (PropTypes.object as Requireable).isRequired, + anchor: (PropTypes.oneOfType([ + PropTypes.object as Requireable, PropTypes.string, + ])).isRequired, closingFunction: PropTypes.func, }; diff --git a/web/src/components/TagPicker.tsx b/web/src/components/TagPicker.tsx index 163ed97b..cb1690fd 100644 --- a/web/src/components/TagPicker.tsx +++ b/web/src/components/TagPicker.tsx @@ -1,5 +1,5 @@ import React, { - Requireable, useRef, useState, + Requireable, useState, } from 'react'; import PropTypes from 'prop-types'; import { @@ -9,27 +9,28 @@ import { useId } from '@fluentui/react-hooks'; import { ICard } from '../controllers/Card'; import { useApp } from '../AppContext'; import { Tag } from './Tag'; -import { ITagEditorProps, TagEditor } from './TagEditor'; import { ITagProperties } from '../controllers/Tag'; +import { TagWrapper } from './TagWrapper'; export interface ITagPickerProps { targetCard: ICard; + editing: boolean; } -export const TagPicker: React.VoidFunctionComponent = ({ targetCard }) => { +export const TagPicker: React.VoidFunctionComponent = ({ + targetCard, editing, +}) => { const user = useApp(); const [pickerActive, setPickerActive] = useState(false); const [tagSearchString, setTagSearchString] = useState(''); - const [focusedTag, setFocusedTag] = useState(); const pickerTargetId = useId('picker-target'); - // map stores tag id keys and element values - const tagTargetRefs = useRef(new Map()); function getNewTag(): void { const tagProps: Partial = { label: tagSearchString || 'New Tag', }; user.newTag(tagProps); + setTagSearchString(''); } // user variable is used to access the tags available to the user @@ -41,16 +42,27 @@ export const TagPicker: React.VoidFunctionComponent = ({ target {/* could style to fit measurements */} - {targetCard?.tags.map((t) => { }} />)} + {targetCard?.tags.map((t) => ( + { + targetCard.update({ tags: targetCard.tags.filter((tag) => tag.id !== t.id) }); + }} + /> + ))} - setPickerActive((old) => !old)} /> + {editing + && setPickerActive((old) => !old)} />} {pickerActive ? ( setPickerActive(false)} + onDismiss={() => { + setPickerActive(false); + }} > @@ -59,35 +71,18 @@ export const TagPicker: React.VoidFunctionComponent = ({ target value={tagSearchString} onChange={(event) => setTagSearchString(event.currentTarget.value)} /> - + {/* Tags available to use are listed here */} {user.user?.tags .filter((t) => t.label.toUpperCase().startsWith(tagSearchString.toUpperCase())) .filter((t) => !targetCard?.tags.includes(t)) .map((t) => ( -
tagTargetRefs.current.set(t.id, el)}> - - - setFocusedTag({ - tag: t, - anchor: tagTargetRefs.current.get(t.id), - })} - /> - -
+ + + ))}
- {focusedTag - && ( - setFocusedTag(undefined)} - /> - )}
) : null} @@ -97,4 +92,5 @@ export const TagPicker: React.VoidFunctionComponent = ({ target TagPicker.propTypes = { targetCard: (PropTypes.object as Requireable).isRequired, + editing: PropTypes.bool.isRequired, }; diff --git a/web/src/components/TagWrapper.tsx b/web/src/components/TagWrapper.tsx new file mode 100644 index 00000000..1a9c8427 --- /dev/null +++ b/web/src/components/TagWrapper.tsx @@ -0,0 +1,54 @@ +import { DefaultButton } from '@fluentui/react'; +import { useId } from '@fluentui/react-hooks'; +import React, { Requireable, useState } from 'react'; +import PropType from 'prop-types'; +import { ICard } from '../controllers/Card'; +import { ITag } from '../controllers/Tag'; +import { Tag } from './Tag'; +import { TagEditor } from './TagEditor'; + +export interface ITagWrapperProps { + tag: ITag; + card?: ICard; +} + +export const TagWrapper: React.VoidFunctionComponent = ({ + tag, card, +}) => { + const targetElemId = useId(); + const [editorOpen, setEditorOpen] = useState(false); + + const attachTag = () => { + if (card && !card.tags.includes(tag)) { + card.update({ tags: [...card.tags, tag] }); + } + }; + + return ( +
+ + setEditorOpen(true)} + /> + {editorOpen + && ( + setEditorOpen(false)} + /> + )} +
+ + ); +}; + +TagWrapper.propTypes = { + tag: (PropType.object as Requireable).isRequired, + card: (PropType.object as Requireable), +}; + +TagWrapper.defaultProps = { + card: undefined, +}; diff --git a/web/src/components/cardDetails/CardDetails.tsx b/web/src/components/cardDetails/CardDetails.tsx index dbf51830..54498a01 100644 --- a/web/src/components/cardDetails/CardDetails.tsx +++ b/web/src/components/cardDetails/CardDetails.tsx @@ -71,7 +71,7 @@ export const CardDetails: React.VoidFunctionComponent = ({ ed - + {mFields.map((field) => ( From 1ee9dd838497fa187fd51c3221e9abcce5f5d852 Mon Sep 17 00:00:00 2001 From: walter Van Date: Fri, 24 Sep 2021 17:45:29 +1000 Subject: [PATCH 05/10] moved TagEditor outside of TagWrapper. Flat callout structure allows both dismissal calls to be triggered when clicked outside TagPicker's primary dropout --- web/src/components/TagPicker.tsx | 55 +++++++++++++++++++++++-------- web/src/components/TagWrapper.tsx | 28 +++++++--------- 2 files changed, 53 insertions(+), 30 deletions(-) diff --git a/web/src/components/TagPicker.tsx b/web/src/components/TagPicker.tsx index cb1690fd..045274a9 100644 --- a/web/src/components/TagPicker.tsx +++ b/web/src/components/TagPicker.tsx @@ -9,59 +9,72 @@ import { useId } from '@fluentui/react-hooks'; import { ICard } from '../controllers/Card'; import { useApp } from '../AppContext'; import { Tag } from './Tag'; -import { ITagProperties } from '../controllers/Tag'; +import { ITag, ITagProperties } from '../controllers/Tag'; import { TagWrapper } from './TagWrapper'; +import { ITagEditorProps, TagEditor } from './TagEditor'; export interface ITagPickerProps { targetCard: ICard; editing: boolean; } +type ITagAnchor = Pick; + export const TagPicker: React.VoidFunctionComponent = ({ targetCard, editing, }) => { const user = useApp(); const [pickerActive, setPickerActive] = useState(false); const [tagSearchString, setTagSearchString] = useState(''); + const [focusedTag, setFocusedTag] = useState(); const pickerTargetId = useId('picker-target'); function getNewTag(): void { const tagProps: Partial = { label: tagSearchString || 'New Tag', }; + // newTag refreshes context and may lead to loss of data. user.newTag(tagProps); setTagSearchString(''); } + function removeTag(t: ITag): void { + targetCard.update({ tags: targetCard.tags.filter((that) => that.id !== t.id) }); + } + // user variable is used to access the tags available to the user return ( Tags - + {/* could style to fit measurements */} - {targetCard?.tags.map((t) => ( - { - targetCard.update({ tags: targetCard.tags.filter((tag) => tag.id !== t.id) }); - }} - /> - ))} + {targetCard?.tags.map((t) => { + if (editing) { + return ( + removeTag(t)} + /> + ); + } + + return ; + })} {editing - && setPickerActive((old) => !old)} />} + && setPickerActive((old) => !old)} id={pickerTargetId} />} {pickerActive ? ( { - setPickerActive(false); + if (!focusedTag) setPickerActive(false); }} > @@ -79,13 +92,27 @@ export const TagPicker: React.VoidFunctionComponent = ({ .filter((t) => !targetCard?.tags.includes(t)) .map((t) => ( - + setFocusedTag({ anchor: id, tag: t })} + /> ))} ) : null} + {focusedTag + && ( + setFocusedTag(undefined)} + /> + )} ); }; diff --git a/web/src/components/TagWrapper.tsx b/web/src/components/TagWrapper.tsx index 1a9c8427..aa5fd823 100644 --- a/web/src/components/TagWrapper.tsx +++ b/web/src/components/TagWrapper.tsx @@ -1,22 +1,21 @@ import { DefaultButton } from '@fluentui/react'; import { useId } from '@fluentui/react-hooks'; -import React, { Requireable, useState } from 'react'; +import React, { Requireable } from 'react'; import PropType from 'prop-types'; import { ICard } from '../controllers/Card'; import { ITag } from '../controllers/Tag'; import { Tag } from './Tag'; -import { TagEditor } from './TagEditor'; export interface ITagWrapperProps { tag: ITag; card?: ICard; + setTagEdit?: (id: string) => void; } export const TagWrapper: React.VoidFunctionComponent = ({ - tag, card, + tag, card, setTagEdit, }) => { const targetElemId = useId(); - const [editorOpen, setEditorOpen] = useState(false); const attachTag = () => { if (card && !card.tags.includes(tag)) { @@ -27,18 +26,13 @@ export const TagWrapper: React.VoidFunctionComponent = ({ return (
- setEditorOpen(true)} - /> - {editorOpen - && ( - setEditorOpen(false)} - /> - )} + {setTagEdit + && ( + setTagEdit(targetElemId)} + /> + )}
); @@ -47,8 +41,10 @@ export const TagWrapper: React.VoidFunctionComponent = ({ TagWrapper.propTypes = { tag: (PropType.object as Requireable).isRequired, card: (PropType.object as Requireable), + setTagEdit: PropType.func, }; TagWrapper.defaultProps = { card: undefined, + setTagEdit: undefined, }; From 837169344c3df59f690d338323298868fe99fcde Mon Sep 17 00:00:00 2001 From: walter Van Date: Fri, 24 Sep 2021 18:19:23 +1000 Subject: [PATCH 06/10] written render tests for TagPicker, TagWrapper, and TagEditor --- web/src/__tests__/components/TagEditor.tsx | 44 ++++ web/src/__tests__/components/TagPicker.tsx | 133 ++++++++++++ web/src/__tests__/components/TagWrapper.tsx | 221 ++++++++++++++++++++ 3 files changed, 398 insertions(+) create mode 100644 web/src/__tests__/components/TagEditor.tsx create mode 100644 web/src/__tests__/components/TagPicker.tsx create mode 100644 web/src/__tests__/components/TagWrapper.tsx diff --git a/web/src/__tests__/components/TagEditor.tsx b/web/src/__tests__/components/TagEditor.tsx new file mode 100644 index 00000000..eca158f4 --- /dev/null +++ b/web/src/__tests__/components/TagEditor.tsx @@ -0,0 +1,44 @@ +import React from 'react'; +import { create } from 'react-test-renderer'; +import { TagEditor } from '../../components/TagEditor'; +import { ITag } from '../../controllers/Tag'; + +import '../disable-icon-warnings.helpers'; + +const demoTag: ITag = { + id: '1', + label: 'Big Boss', + color: '#BF7829', + commit() { throw new Error('Not implemented'); }, + delete() { throw new Error('Not implemented'); }, +}; + +const htmlId = 'TagAnchor'; + +describe('TagEditor render tests', () => { + test('without closingFunction', () => { + const json = create( + , + ).toJSON(); + expect(json).toMatchInlineSnapshot(` + +`); + }); + + test('with closingFunction', () => { + const json = create( + { }} + />, + ).toJSON(); + expect(json).toMatchInlineSnapshot(` + +`); + }); +}); diff --git a/web/src/__tests__/components/TagPicker.tsx b/web/src/__tests__/components/TagPicker.tsx new file mode 100644 index 00000000..cd4adc0b --- /dev/null +++ b/web/src/__tests__/components/TagPicker.tsx @@ -0,0 +1,133 @@ +import React from 'react'; +import { create } from 'react-test-renderer'; +import { AppProvider, IAppContext } from '../../AppContext'; +import { TagPicker } from '../../components/TagPicker'; +import { ICard } from '../../controllers/Card'; +import { ITag } from '../../controllers/Tag'; + +import '../disable-icon-warnings.helpers'; + +function notImplemented() { + return new Error('Not Implemented'); +} + +const demoCard: ICard = { + company: 'None', + tags: [], + email: 'noEmail@jmail.com', + favorite: false, + fields: [], + jobTitle: 'Unemployed', + name: 'no name', + phone: '0001', + update() { throw notImplemented(); }, + commit() { throw notImplemented(); }, + delete() { throw notImplemented(); }, +}; + +const demoTag: ITag = { + id: '1', + label: 'Big Boss', + color: '#BF7829', + commit() { throw notImplemented(); }, + delete() { throw notImplemented(); }, +}; + +const demoApp: IAppContext = { + searchQuery: '', + tagQuery: [], + user: { + username: 'username', + settings: {}, + cards: [demoCard], + tags: [demoTag], + }, + update() { throw notImplemented(); }, + showCardDetail() { throw notImplemented(); }, + newCard() { throw notImplemented(); }, + newTag() { throw notImplemented(); }, + login() { return Promise.reject(notImplemented()); }, + logout() { return Promise.reject(notImplemented()); }, +}; + +describe('TagPicker render tests', () => { + test('TagPicker in editing mode', () => { + const json = create( + + + , + ).toJSON(); + expect(json).toMatchInlineSnapshot(` +
+
+ Tags +
+
+
+
+ +
+`); + }); + + test('TagPicker in read-only', () => { + const json = create( + + + , + ).toJSON(); + expect(json).toMatchInlineSnapshot(` +
+
+ Tags +
+
+
+
+
+`); + }); +}); diff --git a/web/src/__tests__/components/TagWrapper.tsx b/web/src/__tests__/components/TagWrapper.tsx new file mode 100644 index 00000000..341e490a --- /dev/null +++ b/web/src/__tests__/components/TagWrapper.tsx @@ -0,0 +1,221 @@ +import React from 'react'; +import { create } from 'react-test-renderer'; +import { TagWrapper } from '../../components/TagWrapper'; +import { ICard } from '../../controllers/Card'; +import { ITag } from '../../controllers/Tag'; + +import '../disable-icon-warnings.helpers'; + +const demoTag: ITag = { + id: '1', + label: 'Big Boss', + color: '#BF7829', + commit() { throw new Error('Not implemented'); }, + delete() { throw new Error('Not implemented'); }, +}; + +const demoCard: ICard = { + company: 'None', + tags: [], + email: 'noEmail@jmail.com', + favorite: false, + fields: [], + jobTitle: 'Unemployed', + name: 'no name', + phone: '0001', + update() { throw new Error('Not Implemented'); }, + commit() { throw new Error('Not implemented'); }, + delete() { throw new Error('Not implemented'); }, +}; + +describe('TagWrapper Render Tests', () => { + test('with edit button', () => { + const json = create( + { }} + />, + ).toJSON(); + expect(json).toMatchInlineSnapshot(` +
+
+ +
+ +
+`); + }); + + test('without edit button', () => { + const json = create( + , + ).toJSON(); + expect(json).toMatchInlineSnapshot(` +
+
+ +
+
+`); + }); + + test('with attached card, with edit button', () => { + const json = create( + { }} + />, + ).toJSON(); + expect(json).toMatchInlineSnapshot(` +
+
+ +
+ +
+`); + }); +}); From dad720ff7a709df03fb09dad61f0853e72eb413e Mon Sep 17 00:00:00 2001 From: walter Van Date: Sat, 25 Sep 2021 13:51:14 +1000 Subject: [PATCH 07/10] changed to display tags in tagPicker present in targetCard --- web/src/components/TagPicker.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/web/src/components/TagPicker.tsx b/web/src/components/TagPicker.tsx index 045274a9..7f652b1b 100644 --- a/web/src/components/TagPicker.tsx +++ b/web/src/components/TagPicker.tsx @@ -89,7 +89,6 @@ export const TagPicker: React.VoidFunctionComponent = ({ {/* Tags available to use are listed here */} {user.user?.tags .filter((t) => t.label.toUpperCase().startsWith(tagSearchString.toUpperCase())) - .filter((t) => !targetCard?.tags.includes(t)) .map((t) => ( Date: Sat, 25 Sep 2021 15:56:19 +1000 Subject: [PATCH 08/10] moved TagEditor, TagPicker, TagWrapper into subdirectory /components/tagSelector --- web/src/__tests__/components/TagEditor.tsx | 2 +- web/src/__tests__/components/TagPicker.tsx | 2 +- web/src/__tests__/components/TagWrapper.tsx | 2 +- web/src/components/cardDetails/CardDetails.tsx | 2 +- web/src/components/{ => tagSelector}/TagEditor.tsx | 2 +- web/src/components/{ => tagSelector}/TagPicker.tsx | 8 ++++---- web/src/components/{ => tagSelector}/TagWrapper.tsx | 6 +++--- 7 files changed, 12 insertions(+), 12 deletions(-) rename web/src/components/{ => tagSelector}/TagEditor.tsx (97%) rename web/src/components/{ => tagSelector}/TagPicker.tsx (95%) rename web/src/components/{ => tagSelector}/TagWrapper.tsx (89%) diff --git a/web/src/__tests__/components/TagEditor.tsx b/web/src/__tests__/components/TagEditor.tsx index eca158f4..1c059d78 100644 --- a/web/src/__tests__/components/TagEditor.tsx +++ b/web/src/__tests__/components/TagEditor.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { create } from 'react-test-renderer'; -import { TagEditor } from '../../components/TagEditor'; +import { TagEditor } from '../../components/tagSelector/TagEditor'; import { ITag } from '../../controllers/Tag'; import '../disable-icon-warnings.helpers'; diff --git a/web/src/__tests__/components/TagPicker.tsx b/web/src/__tests__/components/TagPicker.tsx index cd4adc0b..69caaf7f 100644 --- a/web/src/__tests__/components/TagPicker.tsx +++ b/web/src/__tests__/components/TagPicker.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { create } from 'react-test-renderer'; import { AppProvider, IAppContext } from '../../AppContext'; -import { TagPicker } from '../../components/TagPicker'; +import { TagPicker } from '../../components/tagSelector/TagPicker'; import { ICard } from '../../controllers/Card'; import { ITag } from '../../controllers/Tag'; diff --git a/web/src/__tests__/components/TagWrapper.tsx b/web/src/__tests__/components/TagWrapper.tsx index 341e490a..03bddf3f 100644 --- a/web/src/__tests__/components/TagWrapper.tsx +++ b/web/src/__tests__/components/TagWrapper.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { create } from 'react-test-renderer'; -import { TagWrapper } from '../../components/TagWrapper'; +import { TagWrapper } from '../../components/tagSelector/TagWrapper'; import { ICard } from '../../controllers/Card'; import { ITag } from '../../controllers/Tag'; diff --git a/web/src/components/cardDetails/CardDetails.tsx b/web/src/components/cardDetails/CardDetails.tsx index 54498a01..def55fb9 100644 --- a/web/src/components/cardDetails/CardDetails.tsx +++ b/web/src/components/cardDetails/CardDetails.tsx @@ -8,7 +8,7 @@ import { CardExtraField } from './CardExtraField'; import { CardImageField } from './CardImageField'; import { CardMandatoryField } from './CardMandatoryField'; import { CardNoteField } from './CardNoteField'; -import { TagPicker } from '../TagPicker'; +import { TagPicker } from '../tagSelector/TagPicker'; export interface ICardDetailsProps { card: ICard; diff --git a/web/src/components/TagEditor.tsx b/web/src/components/tagSelector/TagEditor.tsx similarity index 97% rename from web/src/components/TagEditor.tsx rename to web/src/components/tagSelector/TagEditor.tsx index 40cfa365..c614a00f 100644 --- a/web/src/components/TagEditor.tsx +++ b/web/src/components/tagSelector/TagEditor.tsx @@ -3,7 +3,7 @@ import { } from '@fluentui/react'; import PropTypes, { Requireable } from 'prop-types'; import React, { useState } from 'react'; -import { ITag, ITagProperties } from '../controllers/Tag'; +import { ITag, ITagProperties } from '../../controllers/Tag'; // closing Function is called to dismiss callout export interface ITagEditorProps { diff --git a/web/src/components/TagPicker.tsx b/web/src/components/tagSelector/TagPicker.tsx similarity index 95% rename from web/src/components/TagPicker.tsx rename to web/src/components/tagSelector/TagPicker.tsx index 7f652b1b..cfe7c8e8 100644 --- a/web/src/components/TagPicker.tsx +++ b/web/src/components/tagSelector/TagPicker.tsx @@ -6,10 +6,10 @@ import { Callout, DefaultButton, Stack, TextField, } from '@fluentui/react'; import { useId } from '@fluentui/react-hooks'; -import { ICard } from '../controllers/Card'; -import { useApp } from '../AppContext'; -import { Tag } from './Tag'; -import { ITag, ITagProperties } from '../controllers/Tag'; +import { ICard } from '../../controllers/Card'; +import { useApp } from '../../AppContext'; +import { Tag } from '../Tag'; +import { ITag, ITagProperties } from '../../controllers/Tag'; import { TagWrapper } from './TagWrapper'; import { ITagEditorProps, TagEditor } from './TagEditor'; diff --git a/web/src/components/TagWrapper.tsx b/web/src/components/tagSelector/TagWrapper.tsx similarity index 89% rename from web/src/components/TagWrapper.tsx rename to web/src/components/tagSelector/TagWrapper.tsx index aa5fd823..dd3a277c 100644 --- a/web/src/components/TagWrapper.tsx +++ b/web/src/components/tagSelector/TagWrapper.tsx @@ -2,9 +2,9 @@ import { DefaultButton } from '@fluentui/react'; import { useId } from '@fluentui/react-hooks'; import React, { Requireable } from 'react'; import PropType from 'prop-types'; -import { ICard } from '../controllers/Card'; -import { ITag } from '../controllers/Tag'; -import { Tag } from './Tag'; +import { ICard } from '../../controllers/Card'; +import { ITag } from '../../controllers/Tag'; +import { Tag } from '../Tag'; export interface ITagWrapperProps { tag: ITag; From 0b4bf9eb1daaf21b31dd6782da88d321f1eddff5 Mon Sep 17 00:00:00 2001 From: walter Van Date: Sun, 26 Sep 2021 10:43:14 +1000 Subject: [PATCH 09/10] minor code style changes --- web/src/components/tagSelector/TagEditor.tsx | 29 ++++++++------ web/src/components/tagSelector/TagPicker.tsx | 38 +++++++++---------- web/src/components/tagSelector/TagWrapper.tsx | 1 - 3 files changed, 35 insertions(+), 33 deletions(-) diff --git a/web/src/components/tagSelector/TagEditor.tsx b/web/src/components/tagSelector/TagEditor.tsx index c614a00f..9164e41f 100644 --- a/web/src/components/tagSelector/TagEditor.tsx +++ b/web/src/components/tagSelector/TagEditor.tsx @@ -1,7 +1,7 @@ import { Callout, DefaultButton, ICalloutProps, Stack, TextField, mergeStyles, } from '@fluentui/react'; -import PropTypes, { Requireable } from 'prop-types'; +import PropTypes, { Requireable, Validator } from 'prop-types'; import React, { useState } from 'react'; import { ITag, ITagProperties } from '../../controllers/Tag'; @@ -12,12 +12,12 @@ export interface ITagEditorProps { closingFunction?: () => void; } -const getSwatchClassNames = (selectedColour: string) => { +const getSwatchClassNames = (selectedColor: string) => { const height = 24; const borderRadius = height / 2; const width = 24; // Extracted from Figma - const colours = [ + const colors = [ '#BF7829', '#127976', '#D61317', @@ -32,15 +32,15 @@ const getSwatchClassNames = (selectedColour: string) => { '#FBE900', ]; - return colours.map((colour) => [colour, + return colors.map((color) => [color, mergeStyles([{ height, width, borderRadius, - backgroundColor: colour, + backgroundColor: color, }, - // when selectedColour matches then we have feedback on what colour is selected - (colour === selectedColour) && { + // when selectedColor matches then we have feedback on what color is selected + (color === selectedColor) && { border: '2px solid black', boxSizing: 'border-box', }])]); @@ -93,14 +93,21 @@ export const TagEditor: React.VoidFunctionComponent = ({ ); }; +function requireable(validator: Validator) { + const asRequireable = validator as unknown as Requireable; + const isRequired = asRequireable.isRequired ?? validator; + return { isRequired }; +} +function ensureNotNull(value: T | null | undefined) { + if (value == null) throw new Error('value is nullish'); + return value; +} TagEditor.propTypes = { tag: (PropTypes.object as Requireable).isRequired, - anchor: (PropTypes.oneOfType([ - PropTypes.object as Requireable, PropTypes.string, - ])).isRequired, + anchor: requireable(ensureNotNull(Callout.propTypes?.target)).isRequired, closingFunction: PropTypes.func, }; TagEditor.defaultProps = { - closingFunction: () => { }, + closingFunction: () => {}, }; diff --git a/web/src/components/tagSelector/TagPicker.tsx b/web/src/components/tagSelector/TagPicker.tsx index cfe7c8e8..8c089d49 100644 --- a/web/src/components/tagSelector/TagPicker.tsx +++ b/web/src/components/tagSelector/TagPicker.tsx @@ -24,8 +24,8 @@ export const TagPicker: React.VoidFunctionComponent = ({ targetCard, editing, }) => { const user = useApp(); - const [pickerActive, setPickerActive] = useState(false); - const [tagSearchString, setTagSearchString] = useState(''); + const [pickerActive, setPickerActive] = useState(false); + const [tagSearchString, setTagSearchString] = useState(''); const [focusedTag, setFocusedTag] = useState(); const pickerTargetId = useId('picker-target'); @@ -51,19 +51,15 @@ export const TagPicker: React.VoidFunctionComponent = ({ {/* could style to fit measurements */} - {targetCard?.tags.map((t) => { - if (editing) { - return ( - removeTag(t)} - /> - ); - } - - return ; - })} + {targetCard?.tags.map((t) => ((editing) + ? ( + removeTag(t)} + /> + ) + : ))} @@ -105,12 +101,12 @@ export const TagPicker: React.VoidFunctionComponent = ({ : null} {focusedTag && ( - setFocusedTag(undefined)} - /> + setFocusedTag(undefined)} + /> )} ); diff --git a/web/src/components/tagSelector/TagWrapper.tsx b/web/src/components/tagSelector/TagWrapper.tsx index dd3a277c..5cb2d468 100644 --- a/web/src/components/tagSelector/TagWrapper.tsx +++ b/web/src/components/tagSelector/TagWrapper.tsx @@ -34,7 +34,6 @@ export const TagWrapper: React.VoidFunctionComponent = ({ /> )}
- ); }; From 0a8f568ae4960c02f38cce73c6676221cd0b2c6f Mon Sep 17 00:00:00 2001 From: walter Van Date: Mon, 27 Sep 2021 14:14:12 +1000 Subject: [PATCH 10/10] reverted TagEditor Proptypes --- web/src/components/tagSelector/TagEditor.tsx | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/web/src/components/tagSelector/TagEditor.tsx b/web/src/components/tagSelector/TagEditor.tsx index 9164e41f..82a8e8f0 100644 --- a/web/src/components/tagSelector/TagEditor.tsx +++ b/web/src/components/tagSelector/TagEditor.tsx @@ -1,7 +1,7 @@ import { Callout, DefaultButton, ICalloutProps, Stack, TextField, mergeStyles, } from '@fluentui/react'; -import PropTypes, { Requireable, Validator } from 'prop-types'; +import PropTypes, { Requireable } from 'prop-types'; import React, { useState } from 'react'; import { ITag, ITagProperties } from '../../controllers/Tag'; @@ -93,18 +93,11 @@ export const TagEditor: React.VoidFunctionComponent = ({ ); }; -function requireable(validator: Validator) { - const asRequireable = validator as unknown as Requireable; - const isRequired = asRequireable.isRequired ?? validator; - return { isRequired }; -} -function ensureNotNull(value: T | null | undefined) { - if (value == null) throw new Error('value is nullish'); - return value; -} TagEditor.propTypes = { tag: (PropTypes.object as Requireable).isRequired, - anchor: requireable(ensureNotNull(Callout.propTypes?.target)).isRequired, + anchor: (PropTypes.oneOfType([ + PropTypes.object as Requireable, PropTypes.string, + ])).isRequired, closingFunction: PropTypes.func, };