test/test.ts -------------------------------------------------------------------------------- import { expect } from 'chai'; import 'mocha'; const { JSDOM } = require('jsdom'); const dom = new JSDOM('', { url: `http://localhost:${resolvedPorts.server}`, }); (global as any).window = dom.window; import { reaction } from 'mobx'; import { resolvedPorts } from '../src/client/util/CurrentUserUtils'; import { Doc } from '../src/fields/Doc'; import { createSchema, defaultSpec, makeInterface } from '../src/fields/Schema'; import { Cast } from '../src/fields/Types'; import { ImageField } from '../src/fields/URLField'; describe('Document', () => { it('should hold fields', () => { const key = 'Test'; const key2 = 'Test2'; const field = 15; const doc = new Doc(); doc[key] = field; const getField = Cast(doc[key], 'number'); const getField2 = Cast(doc[key2], 'number'); expect(getField).to.equal(field); expect(getField2).to.equal(undefined); }); it('should update', () => { const doc = new Doc(); const key = 'Test'; const key2 = 'Test2'; let ran = false; reaction( () => doc[key], field => { ran = true; } ); expect(ran).to.equal(false); doc[key2] = 4; expect(ran).to.equal(false); doc[key] = 5; expect(ran).to.equal(true); }); }); const testSchema1 = createSchema({ a: 'number', b: 'string', c: 'boolean', d: ImageField, e: Doc, }); type TestDoc = makeInterface<[typeof testSchema1]>; const TestDoc = makeInterface(testSchema1); const testSchema2 = createSchema({ a: defaultSpec('boolean', true), b: defaultSpec('number', 5), c: defaultSpec('string', 'hello world'), }); type TestDoc2 = makeInterface<[typeof testSchema2]>; const TestDoc2 = makeInterface(testSchema2); const testSchema3 = createSchema({ a: TestDoc2, }); type TestDoc3 = makeInterface<[typeof testSchema3]>; const TestDoc3 = makeInterface(testSchema3); describe('Schema', () => { it('should do the right thing 1', () => { const test1 = new Doc(); const test2 = new Doc(); const ifield = new ImageField(new URL('http://google.com')); test1.a = 5; test1.b = 'hello'; test1.c = true; test1.d = ifield; test1.e = test2; const doc = TestDoc(test1); expect(doc.a).to.equal(5); expect(doc.b).to.equal('hello'); expect(doc.c).to.equal(true); expect(doc.d).to.equal(ifield); expect(doc.e).to.equal(test2); }); it('should do the right thing 2', () => { const test1 = new Doc(); const test2 = new Doc(); const ifield = new ImageField(new URL('http://google.com')); test1.a = 'hello'; test1.b = 5; test1.c = test2; test1.d = true; test1.e = ifield; const doc = TestDoc(test1); expect(doc.a).to.equal(undefined); expect(doc.b).to.equal(undefined); expect(doc.c).to.equal(undefined); expect(doc.d).to.equal(undefined); expect(doc.e).to.equal(undefined); }); it('should do the right thing 3', () => { const test1 = new Doc(); const test2 = new Doc(); const ifield = new ImageField(new URL('http://google.com')); test1.a = 'hello'; test1.b = 5; test1.c = test2; test1.d = true; test1.e = ifield; const doc = TestDoc(test1); expect(doc.a).to.equal(undefined); expect(doc.b).to.equal(undefined); expect(doc.c).to.equal(undefined); expect(doc.d).to.equal(undefined); expect(doc.e).to.equal(undefined); }); it('should do the right thing 4', () => { const doc = TestDoc2(); expect(doc.a).to.equal(true); expect(doc.b).to.equal(5); expect(doc.c).to.equal('hello world'); const d2 = new Doc(); d2.a = false; d2.b = 4; d2.c = 'goodbye'; const doc2 = TestDoc2(d2); expect(doc2.a).to.equal(false); expect(doc2.b).to.equal(4); expect(doc2.c).to.equal('goodbye'); const d3 = new Doc(); d3.a = 'hello'; d3.b = false; d3.c = 5; const doc3 = TestDoc2(d3); expect(doc3.a).to.equal(true); expect(doc3.b).to.equal(5); expect(doc3.c).to.equal('hello world'); }); it('should do the right thing 5', async () => { const test1 = new Doc(); const test2 = new Doc(); const doc = TestDoc3(test1); expect(doc.a).to.equal(undefined); test1.a = test2; const doc2 = (await doc.a)!; expect(doc2.a).to.equal(true); expect(doc2.b).to.equal(5); expect(doc2.c).to.equal('hello world'); }); }); ================================================================================ packages/components/src/index.ts -------------------------------------------------------------------------------- export * from './components' export * from './global' ================================================================================ packages/components/src/components/index.ts -------------------------------------------------------------------------------- export * from './Button' export * from './ColorPicker' export * from './Dropdown' export * from './EditableText' export * from './MultiToggle' export * from './IconButton' export * from './ListBox' export * from './Popup' export * from './Modal' export * from './Group' export * from './Slider' export * from './Toggle' export * from './ListItem' export * from './Overlay' export * from './NumberDropdown' export * from './NumberInput' ================================================================================ packages/components/src/components/NumberDropdown/NumberDropdown.stories.tsx -------------------------------------------------------------------------------- import { Meta, Story } from '@storybook/react' import React, { useState } from 'react' import { INumberDropdownProps, NumberDropdown } from './NumberDropdown' import { Size , getFormLabelSize } from '../../global' export default { title: 'Dash/NumberDropdown', component: NumberDropdown, argTypes: {}, } as Meta // const [number, setNumber] = useState(0) const Template: Story = (args) => console.log(val)} /> export const NumberInputOne = Template.bind({}) NumberInputOne.args = { min: 0, max: 50, step: 1, // number: number, // setNumber: setNumber, width: 100, height: 100, size: Size.SMALL, numberDropdownType: 'slider' } export const NumberInputTwo = Template.bind({}) NumberInputTwo.args = { min: 0, max: 50, step: 2, numberDropdownType: 'dropdown' } ================================================================================ packages/components/src/components/NumberDropdown/NumberDropdown.tsx -------------------------------------------------------------------------------- import * as React from 'react'; import { Colors, INumberProps, Size, getFormLabelSize } from '../../global'; import { Popup } from '../Popup'; import { Toggle, ToggleType } from '../Toggle'; import { useState } from 'react'; import { Slider } from '../Slider'; import { ListBox } from '../ListBox'; import { IListItemProps } from '../ListItem'; import { Group } from '../Group'; import { IconButton } from '../IconButton'; import * as fa from 'react-icons/fa'; import './NumberDropdown.scss'; export type NumberDropdownType = 'slider' | 'dropdown' | 'input'; export interface INumberDropdownProps extends INumberProps { numberDropdownType: NumberDropdownType; showPlusMinus?: boolean; } export const NumberDropdown = (props: INumberDropdownProps) => { const [numberLoc, setNumberLoc] = useState(0); const { fillWidth, // numberDropdownType = false, color = Colors.MEDIUM_BLUE, type, formLabelPlacement, showPlusMinus, min, max, unit, background, step = 1, number = numberLoc, setNumber = setNumberLoc, size, formLabel, tooltip, } = props; const [isOpen, setOpen] = useState(false); let toggleText = number.toString(); if (unit) toggleText = toggleText + unit; let toggle = ( setOpen(!isOpen)} /> ); if (showPlusMinus) { toggle = ( } color={color} onClick={e => { e.stopPropagation(); setNumber(number - step); }} fillWidth={fillWidth} tooltip={`Subtract ${step}${unit}`} /> {toggle} } color={color} onClick={e => { e.stopPropagation(); setNumber(number + step); }} fillWidth={fillWidth} tooltip={`Add ${step}${unit}`} /> ); } let popup; switch (numberDropdownType) { case 'dropdown': { const items: IListItemProps[] = []; for (let i = min; i <= max; i += step) { let text = i.toString(); if (unit) text = i.toString() + unit; items.push({ text: text, val: i, style: { textAlign: 'center' }, }); } popup = setNumber(num as number)} items={items} />; } break; case 'slider': default: popup = ; break; case 'input': popup = ; break; } const numberDropdown: JSX.Element = (
); return formLabel ? (
{numberDropdown}
setOpen(!isOpen)} style={{ cursor: 'pointer', height: '25%', fontSize: getFormLabelSize(size) }}> {formLabel}
) : ( numberDropdown ); }; ================================================================================ packages/components/src/components/NumberDropdown/index.ts -------------------------------------------------------------------------------- export * from './NumberDropdown' ================================================================================ packages/components/src/components/Dropdown/Dropdown.tsx -------------------------------------------------------------------------------- import React, { useState } from 'react'; import { FaCaretDown, FaCaretLeft, FaCaretRight, FaCaretUp } from 'react-icons/fa'; import { Popup, PopupTrigger } from '..'; import { Colors, IGlobalProps, Placement, Type, getFontSize, getHeight, getFormLabelSize } from '../../global'; import { IconButton } from '../IconButton'; import { ListBox } from '../ListBox'; import { IListItemProps, ListItem } from '../ListItem'; import './Dropdown.scss'; import { Tooltip } from '@mui/material'; export enum DropdownType { SELECT = 'select', CLICK = 'click', } export interface IDropdownProps extends IGlobalProps { items: IListItemProps[]; placement?: Placement; dropdownType: DropdownType; title?: string; toolTip?: string; closeOnSelect?: boolean; iconProvider?: (active: boolean, placement?: Placement) => JSX.Element; selectedVal?: string; setSelectedVal?: (val: string | number, e?: React.MouseEvent) => unknown; maxItems?: number; uppercase?: boolean; activeChanged?: (isOpen: boolean) => void; onItemDown?: (e: React.PointerEvent, val: number | string) => boolean; // returns whether to select item } /** * * @param props * @returns * * TODO: add support for isMulti, isSearchable * Look at: import Select from "react-select"; */ export const Dropdown = (props: IDropdownProps) => { const { size, height, maxItems, items, dropdownType, selectedVal, toolTip, setSelectedVal, iconProvider, placement = 'bottom-start', tooltip, tooltipPlacement = 'top', inactive, color = Colors.MEDIUM_BLUE, background, closeOnSelect, title = 'Dropdown', type, width, formLabel, formLabelPlacement, fillWidth = true, onItemDown, uppercase, } = props; const [active, setActive] = useState(false); const itemsMap = new Map(); items.forEach(item => { itemsMap.set(item.val, item); }); const getBorderColor = (): Colors | string | undefined => { switch (type) { case Type.PRIM: return undefined; case Type.SEC: return color; case Type.TERT: if (active) return color; else return color; } }; const defaultProperties: React.CSSProperties = { height: getHeight(height, size), width: fillWidth ? '100%' : width, fontWeight: 500, fontSize: getFontSize(size), fontFamily: 'sans-serif', textTransform: uppercase ? 'uppercase' : undefined, borderColor: getBorderColor(), background, color: color, }; const backgroundProperties: React.CSSProperties = { background: background ?? color, }; const getCaretDirection = (isActive: boolean, caretPlacement: Placement = 'left'): JSX.Element => { if (iconProvider) return iconProvider(isActive, caretPlacement); switch (caretPlacement) { default: case 'bottom':return isActive ? : ; case 'right': return isActive ? : ; case 'top': return isActive ? : ; } // prettier-ignore }; const getToggle = () => { switch (dropdownType) { case DropdownType.SELECT: return (
{selectedVal && }
); case DropdownType.CLICK: default: return (
); } }; const setActiveChanged = (isActive: boolean) => { setActive(isActive); props.activeChanged?.(isActive); }; const dropdown: JSX.Element = (
{getToggle()} } placement={placement} tooltip={tooltip} tooltipPlacement={tooltipPlacement} trigger={PopupTrigger.CLICK} isOpen={active} setOpen={setActiveChanged} size={size} fillWidth={true} color={color} background={background} popup={ { setSelectedVal?.(val, e); closeOnSelect && setActive(false); }} size={size} /> } />
); return formLabel ? (
{formLabel}
{dropdown}
) : ( dropdown ); }; ================================================================================ packages/components/src/components/Dropdown/Dropdown.stories.tsx -------------------------------------------------------------------------------- import { Meta, Story } from '@storybook/react' import React from 'react' import * as fa from 'react-icons/fa' import { Dropdown, DropdownType, IDropdownProps } from '..' import { Colors, Size } from '../../global/globalEnums' import { IListItemProps } from '../ListItem' import { Type , getFormLabelSize } from '../../global' export default { title: 'Dash/Dropdown', component: Dropdown, argTypes: {}, } as Meta const Template: Story = (args) => const dropdownItems: IListItemProps[] = [ { text: 'Facebook Marketplace', val: 'facebook-marketplace', shortcut: '⌘F', icon: , description: 'This is the main component that we use in Dash.', }, { text: 'Google', val: 'google', }, { text: 'Airbnb', val: 'airbnb', icon: , }, { text: 'Salesforce', val: 'salesforce', icon: , items: [ { text: 'Slack', val: 'slack', icon: , }, { text: 'Heroku', val: 'heroku', shortcut: '⌘H', icon: , }, ], }, { text: 'Microsoft', val: 'microsoft', icon: , }, ] export const Select = Template.bind({}) Select.args = { title: 'Select company', tooltip: "This should be a tooltip", type: Type.PRIM, dropdownType: DropdownType.SELECT, items: dropdownItems, size: Size.SMALL, selectedVal: 'facebook-marketplace', background: 'blue', color: Colors.WHITE } export const Click = Template.bind({}) Click.args = { title: '', type: Type.TERT, color: 'red', background: 'blue', dropdownType: DropdownType.SELECT, items: dropdownItems, closeOnSelect: true, size: Size.XSMALL, setSelectedVal: (val) => console.log("SET sel = "+ val), onItemDown: (e, val) => { console.log("ITEM DOWN" + val); return true; } //color: Colors.SUCCESS_GREEN } ================================================================================ packages/components/src/components/Dropdown/index.ts -------------------------------------------------------------------------------- export * from './Dropdown' ================================================================================ packages/components/src/components/Popup/Popup.stories.tsx -------------------------------------------------------------------------------- import { Meta, Story } from '@storybook/react' import React from 'react' import * as fa from 'react-icons/fa' import { Colors, Size } from '../../global/globalEnums' import { IPopupProps, Popup, PopupTrigger } from './Popup' import { Overlay } from '../Overlay' export default { title: 'Dash/Popup', component: Popup, argTypes: {}, } as Meta const Template: Story = (args) => (
HELLO WORLD!
) export const Primary = Template.bind({}) Primary.args = { icon: , title: 'Select company', tooltip: 'Popup tooltip', size: Size.SMALL, popup:
Hello world.
} export const Text = Template.bind({}) Text.args = { icon: , text: 'More', tooltip: 'Popup', size: Size.SMALL, popup:
This is a popup element.
} export const Hover = Template.bind({}) Hover.args = { icon: , trigger: PopupTrigger.HOVER, text: 'More', tooltip: 'Popup', placement: 'right', size: Size.SMALL, popup:
This is a popup element.
} ================================================================================ packages/components/src/components/Popup/Popup.tsx -------------------------------------------------------------------------------- import React, { useEffect, useRef, useState } from 'react'; import { IGlobalProps, Placement, Size } from '../../global'; import { Toggle, ToggleType } from '../Toggle'; import './Popup.scss'; import { Popper } from '@mui/material'; import PositionObserver from '@thednp/position-observer'; export enum PopupTrigger { CLICK = 'click', HOVER = 'hover', HOVER_DELAY = 'hover_delay', } export interface IPopupProps extends IGlobalProps { text?: string; icon?: JSX.Element | string; iconPlacement?: Placement; placement?: Placement; size?: Size; height?: number | string; toggle?: JSX.Element; popup: JSX.Element | string | (() => JSX.Element); trigger?: PopupTrigger; isOpen?: boolean; setOpen?: (b: boolean) => void; background?: string; showUntilToggle?: boolean; // whether popup stays open when background is clicked. muyst click toggle button tp close it. toggleFunc?: () => void; popupContainsPt?: (x: number, y: number) => boolean; multitoggle?: boolean; } /** * * @param props * @returns * * TODO: add support for isMulti, isSearchable * Look at: import Select from "react-select"; */ export const Popup = (props: IPopupProps) => { const [locIsOpen, locSetOpen] = useState(false); const { text, size, icon, popup, type, color, isOpen = locIsOpen, setOpen = locSetOpen, toggle, tooltip, trigger = PopupTrigger.CLICK, placement = 'bottom-start', width, height, fillWidth, iconPlacement = 'left', background, multitoggle, popupContainsPt, } = props; const triggerRef = useRef(null); const popperRef = useRef(null); const [toggleRef, setToggleRef] = useState(null); let timeout = setTimeout(() => {}); const handlePointerAwayDown = (e: PointerEvent) => { const rect = popperRef.current?.getBoundingClientRect(); const rect2 = toggleRef?.getBoundingClientRect(); if ( !props.showUntilToggle && (!rect2 || !(rect2.left < e.clientX && rect2.top < e.clientY && rect2.right > e.clientX && rect2.bottom > e.clientY)) && rect && !(rect.left < e.clientX && rect.top < e.clientY && rect.right > e.clientX && rect.bottom > e.clientY) && !popupContainsPt?.(e.clientX, e.clientY) ) { e.preventDefault(); setOpen(false); e.stopPropagation(); } }; let observer: PositionObserver | undefined = undefined; const [previousPosition, setPreviousPosition] = useState(toggleRef?.getBoundingClientRect()); useEffect(() => { if (isOpen) { window.removeEventListener('pointerdown', handlePointerAwayDown, { capture: true }); window.addEventListener('pointerdown', handlePointerAwayDown, { capture: true }); if (toggleRef && multitoggle) { (observer = new PositionObserver(entries => { entries.forEach(entry => { const currentPosition = entry.boundingClientRect; if (Math.floor(currentPosition.top) !== Math.floor(previousPosition?.top ?? 0) || Math.floor(currentPosition.left) !== Math.floor(previousPosition?.left ?? 0)) { // Perform actions when position changes setPreviousPosition(currentPosition); // Update previous position } }); })).observe(toggleRef); } return () => { window.removeEventListener('pointerdown', handlePointerAwayDown, { capture: true }); observer?.disconnect(); }; } else observer?.disconnect(); }, [isOpen, toggleRef, popupContainsPt]); return (
trigger === PopupTrigger.CLICK && setOpen(!isOpen)} onPointerEnter={() => { if (trigger === PopupTrigger.HOVER || trigger === PopupTrigger.HOVER_DELAY) { clearTimeout(timeout); setOpen(true); } }} onPointerLeave={() => { if (trigger === PopupTrigger.HOVER || trigger === PopupTrigger.HOVER_DELAY) { timeout = setTimeout(() => setOpen(false), 1000); } }}>
setToggleRef(R)}> {toggle ?? ( { if (trigger === PopupTrigger.CLICK) { setOpen(!isOpen); props.toggleFunc?.(); } }} fillWidth={fillWidth} /> )}
e.stopPropagation()} onPointerEnter={() => { if (trigger === PopupTrigger.HOVER || trigger === PopupTrigger.HOVER_DELAY) { clearTimeout(timeout); setOpen(true); } }} onPointerLeave={() => { if (trigger === PopupTrigger.HOVER || trigger === PopupTrigger.HOVER_DELAY) { timeout = setTimeout(() => setOpen(false), 200); } }}> {!isOpen ? null : typeof popup === 'function' ? popup() : popup}
); }; ================================================================================ packages/components/src/components/Popup/index.ts -------------------------------------------------------------------------------- export * from './Popup' ================================================================================ packages/components/src/components/Group/Group.tsx -------------------------------------------------------------------------------- import React from 'react'; import './Group.scss'; import { Colors, IGlobalProps, getFontSize, isDark, getFormLabelSize } from '../../global'; export interface IGroupProps extends IGlobalProps { children: any; rowGap?: number; columnGap?: number; padding?: number | string; } export const Group = (props: IGroupProps) => { const { children, width = '100%', rowGap = 5, columnGap = 5, padding = 0, formLabel, formLabelPlacement, size, style, fillWidth } = props; const group: JSX.Element = (
{children}
); return formLabel ? (
{formLabel}
{group}
) : ( group ); }; ================================================================================ packages/components/src/components/Group/Group.stories.tsx -------------------------------------------------------------------------------- import { Meta, Story } from '@storybook/react' import React from 'react' import * as bi from 'react-icons/bi' import { Dropdown, DropdownType } from '../Dropdown' import { IconButton } from '../IconButton' import { Popup, PopupTrigger } from '../Popup' import { Group, IGroupProps } from './Group' import { Type , getFormLabelSize } from '../../global' export default { title: 'Dash/Group', component: Group, argTypes: {}, } as Meta const Template: Story = (args) => ( } type={Type.SEC} /> } type={Type.SEC} /> } type={Type.SEC} popup={
HELLO
} /> } type={Type.SEC} fillWidth /> } type={Type.SEC} fillWidth /> } trigger={PopupTrigger.CLICK} placement={'bottom'} popup={ } type={Type.SEC} /> } type={Type.SEC} /> } type={Type.SEC} /> } type={Type.SEC} /> } type={Type.SEC} /> } />
) export const Primary = Template.bind({}) Primary.args = { width: '100%' } ================================================================================ packages/components/src/components/Group/index.ts -------------------------------------------------------------------------------- export * from './Group' ================================================================================ packages/components/src/components/FormInput/index.ts -------------------------------------------------------------------------------- export * from './FormInput' ================================================================================ packages/components/src/components/FormInput/FormInput.stories.tsx -------------------------------------------------------------------------------- import React from 'react'; import { Story, Meta } from '@storybook/react'; import { Colors, Size } from '../../global/globalEnums'; import * as fa from 'react-icons/fa' import { IListBoxItemProps } from '../ListItem'; import { FormInput, IFormInputProps } from './FormInput'; import { IconButton } from '../IconButton'; export default { title: 'Dash/Form Input', component: FormInput, argTypes: {}, } as Meta; const Template: Story = (args) => ; // export const Primary = Template.bind({}); // Primary.args = { // title: 'Hello World!', // initialIsOpen: true, // }; ================================================================================ packages/components/src/components/FormInput/FormInput.tsx -------------------------------------------------------------------------------- import React from 'react' import './FormInput.scss' export interface IFormInputProps { placeholder?: string value?: string title?: string type?: string onChange: (event: React.ChangeEvent) => void } export const FormInput = (props: IFormInputProps) => { const { placeholder, type, value, title, onChange } = props return (
) } ================================================================================ packages/components/src/components/Template/Template.stories.tsx -------------------------------------------------------------------------------- import { Meta, Story } from '@storybook/react' import React from 'react' import { ITemplateProps, Template } from './Template' export default { title: 'Dash/Template', component: Template, argTypes: {}, } as Meta const TemplateStory: Story = (args) =>