use-debounce#useDebouncedCallback TypeScript Examples
The following examples show how to use
use-debounce#useDebouncedCallback.
You can vote up the ones you like or vote down the ones you don't like,
and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: ActionRoleList.tsx From amplication with Apache License 2.0 | 6 votes |
ActionRoleList = ({
availableRoles,
selectedRoleIds,
debounceMS,
onChange,
}: Props) => {
const [selectedRoleList, setSelectedRoleList] = useState<Set<string>>(
selectedRoleIds
);
// onChange is wrapped with a useDebouncedCallback so it won't be called more often than defined in debounceMs
// This function is called by handleRoleSelect as it can not manage dependencies
const [debouncedOnChange] = useDebouncedCallback((value: Set<string>) => {
onChange(value);
}, debounceMS);
const handleRoleSelect = useCallback(
({ id }: models.AppRole, checked: boolean) => {
const newSet = new Set(selectedRoleList);
if (checked) {
newSet.add(id);
} else {
newSet.delete(id);
}
setSelectedRoleList(newSet);
debouncedOnChange(newSet);
},
[setSelectedRoleList, selectedRoleList, debouncedOnChange]
);
return availableRoles.map((role) => (
<ActionRole
key={role.id}
role={role}
onClick={handleRoleSelect}
selected={selectedRoleList.has(role.id)}
/>
));
}
Example #2
Source File: NameField.tsx From amplication with Apache License 2.0 | 6 votes |
NameField = ({ capitalized, ...rest }: Props) => {
const [regexp, pattern, helpText] = capitalized
? [CAPITALIZED_NAME_REGEX, CAPITALIZED_NAME_PATTERN, CAPITALIZED_HELP_TEXT]
: [NAME_REGEX, NAME_PATTERN, HELP_TEXT];
// @ts-ignore
const [field, meta] = useField<string>({
...rest,
validate: (value) => (value.match(regexp) ? undefined : helpText),
});
const [showMessage, setShowMessage] = useState<boolean>(false);
const [debouncedHideMessage] = useDebouncedCallback(() => {
setShowMessage(false);
}, SHOW_MESSAGE_DURATION);
useEffect(() => {
if (meta.error) {
setShowMessage(true);
} else {
debouncedHideMessage();
}
}, [meta.error, setShowMessage, debouncedHideMessage]);
return (
<div className={CLASS_NAME}>
<TextInput
{...field}
{...rest}
label="Name"
autoComplete="off"
minLength={1}
pattern={pattern}
/>
{showMessage && (
<div className={`${CLASS_NAME}__tooltip`}>{helpText}</div>
)}
</div>
);
}
Example #3
Source File: useAccountsData.ts From sail with Apache License 2.0 | 5 votes |
useAccountsData = (
keys: (PublicKey | null | undefined)[]
): readonly AccountDatum[] => {
const { getDatum, onBatchCache, fetchKeys, onError } = useSail();
const [data, setData] = useState<{ [cacheKey: string]: AccountDatum }>(() =>
loadKeysFromCache(getDatum, keys)
);
// TODO: add cancellation
const fetchAndSetKeys = useDebouncedCallback(
async (
fetchKeys: FetchKeysFn,
keys: readonly (PublicKey | null | undefined)[]
) => {
const keysData = await fetchKeysMaybe(fetchKeys, keys);
const nextData: Record<string, AccountDatum> = {};
keys.forEach((key, keyIndex) => {
if (key) {
const keyData = keysData[keyIndex];
if (keyData) {
nextData[getCacheKeyOfPublicKey(key)] = keyData.data;
} else {
nextData[getCacheKeyOfPublicKey(key)] = keyData;
}
}
});
startTransition(() => {
setData(nextData);
});
},
100
);
useEffect(() => {
void (async () => {
await fetchAndSetKeys(fetchKeys, keys)?.catch((e) => {
onError(new SailCacheRefetchError(e, keys));
});
})();
}, [keys, fetchAndSetKeys, fetchKeys, onError]);
useAccountsSubscribe(keys);
// refresh from the cache whenever the cache is updated
useEffect(() => {
return onBatchCache((e) => {
if (keys.find((key) => key && e.hasKey(key))) {
void fetchAndSetKeys(fetchKeys, keys)?.catch((e) => {
onError(new SailCacheRefetchError(e, keys));
});
}
});
}, [keys, fetchAndSetKeys, fetchKeys, onError, onBatchCache]);
// unload debounces when the component dismounts
useEffect(() => {
return () => {
fetchAndSetKeys.cancel();
};
}, [fetchAndSetKeys]);
return useMemo(() => {
return keys.map((key) => {
if (key) {
return data[getCacheKeyOfPublicKey(key)];
}
return key;
});
}, [data, keys]);
}
Example #4
Source File: converter.tsx From genql with MIT License | 5 votes |
function Page({}) {
const [code, setCode] = useState(DEFAULT_QUERY)
const [onCodeChange] = useDebouncedCallback(() => {
setInvalid('')
try {
const query = gql(code)
console.log('parsed')
setGenqlTranslation('\n' + print(query, {}))
} catch (e) {
console.error(e)
setInvalid(e.message)
}
}, 400)
useEffect(() => {
onCodeChange()
return
}, [code])
const [genqlTranslation, setGenqlTranslation] = useState('')
const [invalid, setInvalid] = useState('')
return (
<Stack align='stretch'>
<SectionTitle
dark
heading='Convert graphql to genql'
subheading='Easily migrate from graphql strings to type safe genql queries'
mb='40px'
/>
<Stack p='10' align='stretch'>
<Stack
spacing='20'
justify='stretch'
width='100%'
align={['center', null, null, 'flex-start']}
direction={['column', null, null, 'row']}
>
<Stack minWidth='0' align='stretch' flex='1'>
{invalid && (
<Stack
as='pre'
px='20px'
zIndex={1000}
mb='-40px'
color='red.500'
>
{invalid}
</Stack>
)}
<Code value={code} onChange={setCode} />
</Stack>
<Code
hideLinesNumbers
flex='1'
readOnly
value={genqlTranslation}
/>
</Stack>
</Stack>
</Stack>
)
}
Example #5
Source File: TableCell.tsx From firetable with Apache License 2.0 | 5 votes |
export default function Date_({
rowIdx,
column,
value,
onSubmit,
disabled,
}: IHeavyCellProps) {
const classes = useStyles();
const { dataGridRef } = useFiretableContext();
const transformedValue = transformValue(value);
const [handleDateChange] = useDebouncedCallback<DatePickerProps["onChange"]>(
(date) => {
const sanitized = sanitizeValue(date);
if (sanitized === undefined) return;
onSubmit(sanitized);
if (dataGridRef?.current?.selectCell)
dataGridRef.current.selectCell({ rowIdx, idx: column.idx });
},
500
);
if (disabled)
return (
<div className={classes.disabledCell}>
<BasicCell
value={value}
type={(column as any).type}
name={column.key}
/>
</div>
);
return (
<MuiPickersUtilsProvider utils={DateFnsUtils}>
<KeyboardDatePicker
value={transformedValue}
onChange={handleDateChange}
onClick={(e) => e.stopPropagation()}
format={column.config?.format ?? DATE_FORMAT}
fullWidth
clearable
keyboardIcon={<DateIcon />}
className={clsx("cell-collapse-padding", classes.root)}
inputVariant="standard"
InputProps={{
disableUnderline: true,
classes: { root: classes.inputBase, input: classes.input },
}}
InputAdornmentProps={{
position: "start",
classes: { root: classes.inputAdornment },
}}
KeyboardButtonProps={{
size: "small",
classes: { root: !disabled ? "row-hover-iconButton" : undefined },
}}
DialogProps={{ onClick: (e) => e.stopPropagation() }}
disabled={disabled}
/>
</MuiPickersUtilsProvider>
);
}
Example #6
Source File: TableCell.tsx From firetable with Apache License 2.0 | 5 votes |
export default function DateTime({
rowIdx,
column,
value,
onSubmit,
disabled,
}: IHeavyCellProps) {
const classes = useStyles();
const { dataGridRef } = useFiretableContext();
const transformedValue = transformValue(value);
const [handleDateChange] = useDebouncedCallback<DatePickerProps["onChange"]>(
(date) => {
const sanitized = sanitizeValue(date);
if (sanitized === undefined) return;
onSubmit(sanitized);
if (dataGridRef?.current?.selectCell)
dataGridRef.current.selectCell({ rowIdx, idx: column.idx });
},
500
);
if (disabled)
return (
<div className={classes.disabledCell}>
<BasicCell
value={value}
type={(column as any).type}
name={column.key}
/>
</div>
);
return (
<MuiPickersUtilsProvider utils={DateFnsUtils}>
<KeyboardDateTimePicker
value={transformedValue}
onChange={handleDateChange}
onClick={(e) => e.stopPropagation()}
format={DATE_TIME_FORMAT}
fullWidth
clearable
keyboardIcon={<DateTimeIcon />}
className={clsx("cell-collapse-padding", classes.root)}
inputVariant="standard"
InputProps={{
disableUnderline: true,
classes: { root: classes.inputBase, input: classes.input },
}}
InputAdornmentProps={{
position: "start",
classes: { root: classes.inputAdornment },
}}
KeyboardButtonProps={{
size: "small",
classes: { root: "row-hover-iconButton" },
}}
DialogProps={{ onClick: (e) => e.stopPropagation() }}
dateRangeIcon={<DateRangeIcon className={classes.dateTabIcon} />}
timeIcon={<TimeIcon className={classes.dateTabIcon} />}
/>
</MuiPickersUtilsProvider>
);
}
Example #7
Source File: useFormButton.tsx From SQForm with MIT License | 5 votes |
export function useFormButton<Values>({
isDisabled = false,
shouldRequireFieldUpdates = false,
onClick,
buttonType,
}: UseFormButtonProps): UseFormButtonReturnType<Values> {
const {values, initialValues, isValid, dirty, ...rest} =
useFormikContext<Values>();
const isButtonDisabled = React.useMemo(() => {
if (isDisabled) {
return true;
}
if (buttonType === BUTTON_TYPES.SUBMIT) {
if (!isValid) {
return true;
}
if (shouldRequireFieldUpdates && !dirty) {
return true;
}
}
if (buttonType === BUTTON_TYPES.RESET) {
if (!dirty) {
return true;
}
}
return false;
}, [buttonType, dirty, isDisabled, isValid, shouldRequireFieldUpdates]);
const handleClick = useDebouncedCallback<
React.MouseEventHandler<HTMLButtonElement>
>((event) => onClick && onClick(event), 500, {
leading: true,
trailing: false,
});
return {
isButtonDisabled,
values,
initialValues,
isValid,
handleClick,
dirty,
...rest,
};
}
Example #8
Source File: SQForm.tsx From SQForm with MIT License | 5 votes |
function SQForm<Values extends FormikValues>({
children,
enableReinitialize = false,
initialValues,
muiGridProps = {},
onSubmit,
validationSchema,
}: SQFormProps<Values>): JSX.Element {
const initialErrors = useInitialRequiredErrors(
validationSchema,
initialValues
);
// HACK: This is a workaround for: https://github.com/mui-org/material-ui-pickers/issues/2112
// Remove this reset handler when the issue is fixed.
const handleReset = () => {
document &&
document.activeElement &&
(document.activeElement as HTMLElement).blur();
};
const handleSubmit = useDebouncedCallback(
(values: Values, formikHelpers: FormikHelpers<Values>) =>
onSubmit(values, formikHelpers),
500,
{leading: true, trailing: false}
);
return (
<Formik<Values>
enableReinitialize={enableReinitialize}
initialErrors={initialErrors}
initialValues={initialValues}
onSubmit={handleSubmit}
onReset={handleReset}
validationSchema={validationSchema}
validateOnMount={true}
>
{(_props) => {
return (
<Form>
<Grid
{...muiGridProps}
container
spacing={muiGridProps.spacing ?? 2}
>
{children}
</Grid>
</Form>
);
}}
</Formik>
);
}
Example #9
Source File: EventPropertiesStats.tsx From posthog-foss with MIT License | 4 votes |
export function EventPropertiesStats(): JSX.Element {
const { eventPropertiesDefinitions, eventsSnippet, eventPropertiesDefinitionTags, tagLoading } =
useValues(definitionDrawerLogic)
const { setNewEventPropertyTag, deleteEventPropertyTag, setEventPropertyDescription, saveAll } =
useActions(definitionDrawerLogic)
const propertyExamples = eventsSnippet[0]?.properties
const tableColumns = [
{
title: 'Property',
key: 'property',
render: function renderProperty({ name }: PropertyDefinition) {
return <span className="text-default">{name}</span>
},
},
{
title: 'Description',
key: 'description',
render: function renderDescription({ description, id }: PropertyDefinition) {
const [newDescription, setNewDescription] = useState(description)
const debouncePropertyDescription = useDebouncedCallback((value) => {
setEventPropertyDescription(value, id)
}, 200)
return (
<Input.TextArea
placeholder="Add description"
value={newDescription || ''}
onChange={(e) => {
setNewDescription(e.target.value)
debouncePropertyDescription(e.target.value)
}}
onKeyDown={(e) => {
if (e.key === 'Enter') {
saveAll()
}
}}
/>
)
},
},
{
title: 'Tags',
key: 'tags',
render: function renderTags({ id, tags }: PropertyDefinition) {
return (
<ObjectTags
id={id}
tags={tags || []}
onTagSave={(tag, currentTags, propertyId) =>
setNewEventPropertyTag(tag, currentTags, propertyId)
}
onTagDelete={(tag, currentTags, propertyId) =>
deleteEventPropertyTag(tag, currentTags, propertyId)
}
saving={tagLoading}
tagsAvailable={eventPropertiesDefinitionTags?.filter((tag) => !tags?.includes(tag))}
/>
)
},
},
{
title: 'Example',
key: 'example',
render: function renderExample({ name }: PropertyDefinition) {
let example = propertyExamples[name]
// Just show a json stringify if it's an array or object
example =
Array.isArray(example) || example instanceof Object
? JSON.stringify(example).substr(0, 140)
: example
return (
<div style={{ backgroundColor: '#F0F0F0', padding: '4px, 15px', textAlign: 'center' }}>
<span style={{ fontSize: 10, fontWeight: 400, fontFamily: 'monaco' }}>{example}</span>
</div>
)
},
},
]
return (
<>
<Row style={{ paddingBottom: 16 }}>
<span className="text-default text-muted">
Top properties that are sent with this event. Please note that description and tags are shared
across events. Posthog properties are <b>excluded</b> from this list.
</span>
</Row>
<Table
dataSource={eventPropertiesDefinitions}
columns={tableColumns}
rowKey={(row) => row.id}
size="small"
tableLayout="fixed"
pagination={{ pageSize: 5, hideOnSinglePage: true }}
/>
</>
)
}
Example #10
Source File: PDFViewerPage.tsx From react-view-pdf with MIT License | 4 votes |
PDFViewerPageInner: React.FC<PDFViewerPageProps> = props => {
const { document, pageNumber, scale, onPageVisibilityChanged, onPageLoaded, loaded } = props;
const [page, setPage] = React.useState<PdfJs.Page>();
const [isCalculated, setIsCalculated] = React.useState(false);
const canvasRef = React.createRef<HTMLCanvasElement>();
const renderTask = React.useRef<PdfJs.PageRenderTask>();
const debouncedLoad = useDebouncedCallback(() => loadPage(), 100, { leading: true });
const debouncedRender = useDebouncedCallback(() => renderPage(), 100, { leading: true });
const intersectionThreshold = [...Array(10)].map((_, i) => i / 10);
React.useEffect(() => {
debouncedRender();
}, [page, scale]);
function loadPage() {
if (document && !page && !isCalculated) {
setIsCalculated(true);
document.getPage(pageNumber).then(page => {
const viewport = page.getViewport({ scale: 1 });
onPageLoaded(pageNumber, viewport.width, viewport.height);
setPage(page);
});
}
}
function renderPage() {
if (page) {
const task = renderTask.current;
if (task) {
task.cancel();
}
const canvasEle = canvasRef.current as HTMLCanvasElement;
if (!canvasEle) {
return;
}
const viewport = page.getViewport({ scale });
canvasEle.height = viewport.height;
canvasEle.width = viewport.width;
const canvasContext = canvasEle.getContext('2d', { alpha: false }) as CanvasRenderingContext2D;
renderTask.current = page.render({
canvasContext,
viewport,
});
renderTask.current.promise.then(
// eslint-disable-next-line @typescript-eslint/no-empty-function
() => {},
// eslint-disable-next-line @typescript-eslint/no-empty-function
() => {},
);
}
}
function visibilityChanged(params: VisibilityChanged) {
const ratio = params.isVisible ? params.ratio : 0;
const changed = onPageVisibilityChanged(pageNumber, ratio);
if (params.isVisible && changed) {
debouncedLoad();
}
}
return (
<Page
disableSelect={props.disableSelect}
style={{
width: !loaded && '70%',
height: !loaded && '1200px',
padding: !loaded && distance.large,
}}>
<Observer onVisibilityChanged={visibilityChanged} threshold={intersectionThreshold}>
{!loaded && (
<>
<Skeleton width="30%" height="3em" />
<br />
<br />
{range(5).map(index => (
<React.Fragment key={index}>
<Skeleton width="80%" height="1em" />
<br />
<Skeleton width="70%" height="1em" />
<br />
<Skeleton width="85%" height="1em" />
<br />
<Skeleton width="60%" height="1em" />
<br />
<Skeleton width="80%" height="1em" />
<br />
<Skeleton width="25%" height="1em" />
<br />
<Skeleton width="60%" height="1em" />
<br />
<Skeleton width="38%" height="1em" />
<br />
<Skeleton width="50%" height="1em" />
<br />
</React.Fragment>
))}
</>
)}
<canvas ref={canvasRef} />
</Observer>
</Page>
);
}
Example #11
Source File: SQFormScrollableCard.tsx From SQForm with MIT License | 4 votes |
function SQFormScrollableCard<Values>({
cardContentStyles = {},
children,
enableReinitialize = false,
height,
helperErrorText,
helperFailText,
helperValidText,
initialValues,
isDisabled = false,
isFailedState = false,
isSelfBounding,
muiGridProps = {},
onSubmit,
resetButtonText = 'Reset',
shouldRenderHelperText = true,
shouldRequireFieldUpdates = false,
submitButtonText = 'Submit',
SubHeaderComponent,
title,
validationSchema,
isHeaderDisabled = false,
isSquareCorners = true,
}: SQFormScrollableCardProps<Values>): React.ReactElement {
const hasSubHeader = Boolean(SubHeaderComponent);
const initialErrors = useInitialRequiredErrors(
validationSchema,
initialValues
);
const classes = useStyles({hasSubHeader, cardContentStyles});
const handleSubmit = useDebouncedCallback(
(formValues: Values, formikBag: FormikHelpers<Values>) =>
onSubmit(formValues, formikBag),
500,
{leading: true, trailing: false}
);
const cardID = React.useMemo(() => {
if (title) {
return title.replace(/\s/g, '-');
} else {
// Ensures IDs are present if no title is given and are random
// incase multiple SQFormScrollableCards exist in the dom
// Statistically unique for the lifetime of the components
return (Date.now() * Math.random()).toString();
}
}, [title]);
const [calculatedHeight, setCalculatedHeight] =
React.useState<React.CSSProperties['height']>(0);
React.useEffect(() => {
const currentElement = document.getElementById(
`sqform-scrollable-card-id-${cardID}`
);
if (!currentElement?.parentElement) {
return;
}
const topOffset = currentElement?.getBoundingClientRect().top;
const offsetBasedHeight = `calc(100vh - ${topOffset}px - 24px)`;
const parentHeight = currentElement.parentElement.clientHeight;
const parentTopOffset =
currentElement.parentElement.getBoundingClientRect().top;
const topDifferential = topOffset - parentTopOffset;
const maxOffsetBasedHeight = `calc(${parentHeight}px - ${topDifferential}px)`;
const calculatedHeight = `min(${offsetBasedHeight}, ${maxOffsetBasedHeight})`;
setCalculatedHeight(calculatedHeight);
}, [cardID]);
const heightToUse =
height || (isSelfBounding && calculatedHeight ? calculatedHeight : '100%');
return (
<div
id={`sqform-scrollable-card-id-${cardID}`}
style={{height: heightToUse}}
>
<Formik
enableReinitialize={enableReinitialize}
initialErrors={initialErrors}
initialValues={initialValues}
onSubmit={handleSubmit}
validationSchema={validationSchema}
validateOnMount={true}
>
{(_props) => {
return (
<Form className={classes.form}>
<Card
raised={true}
elevation={1}
square={isSquareCorners}
className={classes.card}
>
{!isHeaderDisabled && (
<CardHeader
title={title}
className={classes.cardHeader}
titleTypographyProps={{variant: 'h5'}}
/>
)}
<CardContent className={classes.cardContent}>
{SubHeaderComponent}
<Grid
{...muiGridProps}
container
spacing={muiGridProps.spacing ?? 2}
className={classes.childrenContainer}
>
{children}
</Grid>
</CardContent>
<CardActions className={classes.cardFooter}>
<SQFormButton type="reset" title="Reset Form">
{resetButtonText}
</SQFormButton>
{shouldRenderHelperText && (
<SQFormHelperText
isFailedState={isFailedState}
errorText={helperErrorText}
failText={helperFailText}
validText={helperValidText}
/>
)}
<SQFormButton
isDisabled={isDisabled}
shouldRequireFieldUpdates={shouldRequireFieldUpdates}
>
{submitButtonText}
</SQFormButton>
</CardActions>
</Card>
</Form>
);
}}
</Formik>
</div>
);
}
Example #12
Source File: FunnelConversionWindowFilter.tsx From posthog-foss with MIT License | 4 votes |
export function FunnelConversionWindowFilter({ horizontal }: { horizontal?: boolean }): JSX.Element {
const { insightProps } = useValues(insightLogic)
const { conversionWindow, aggregationTargetLabel, filters } = useValues(funnelLogic(insightProps))
const { setFilters } = useActions(funnelLogic(insightProps))
const [localConversionWindow, setLocalConversionWindow] = useState<FunnelConversionWindow>(conversionWindow)
const timeUnitRef: React.RefObject<RefSelectProps> | null = useRef(null)
const options = Object.keys(TIME_INTERVAL_BOUNDS).map((unit) => ({
label: pluralize(conversionWindow.funnel_window_interval ?? 7, unit, `${unit}s`, false),
value: unit,
}))
const intervalBounds =
TIME_INTERVAL_BOUNDS[conversionWindow.funnel_window_interval_unit ?? FunnelConversionWindowTimeUnit.Day]
const setConversionWindow = useDebouncedCallback((): void => {
if (
localConversionWindow.funnel_window_interval !== conversionWindow.funnel_window_interval ||
localConversionWindow.funnel_window_interval_unit !== conversionWindow.funnel_window_interval_unit
) {
setFilters(localConversionWindow)
}
}, 200)
return (
<div
className={clsx('funnel-options-container', horizontal && 'flex-center')}
style={horizontal ? { flexDirection: 'row' } : undefined}
>
<span className="funnel-options-label">
Conversion window limit{' '}
<Tooltip
title={
<>
<b>Recommended!</b> Limit to {aggregationTargetLabel.plural}{' '}
{filters.aggregation_group_type_index != undefined ? 'that' : 'who'} converted within a
specific time frame. {capitalizeFirstLetter(aggregationTargetLabel.plural)}{' '}
{filters.aggregation_group_type_index != undefined ? 'that' : 'who'} do not convert in this
time frame will be considered as drop-offs.
</>
}
>
<InfoCircleOutlined className="info-indicator" />
</Tooltip>
</span>
<Row className="funnel-options-inputs" style={horizontal ? { paddingLeft: 8 } : undefined}>
<InputNumber
className="time-value-input"
min={intervalBounds[0]}
max={intervalBounds[1]}
defaultValue={conversionWindow.funnel_window_interval}
value={localConversionWindow.funnel_window_interval}
onChange={(funnel_window_interval) => {
setLocalConversionWindow((state) => ({
...state,
funnel_window_interval: Number(funnel_window_interval),
}))
setConversionWindow()
}}
onBlur={setConversionWindow}
onPressEnter={setConversionWindow}
/>
<Select
ref={timeUnitRef}
className="time-unit-input"
defaultValue={conversionWindow.funnel_window_interval_unit}
dropdownMatchSelectWidth={false}
value={localConversionWindow.funnel_window_interval_unit}
onChange={(funnel_window_interval_unit: FunnelConversionWindowTimeUnit) => {
setLocalConversionWindow((state) => ({ ...state, funnel_window_interval_unit }))
setConversionWindow()
}}
onBlur={setConversionWindow}
>
{options.map(({ value, label }) => (
<Select.Option value={value} key={value}>
{label}
</Select.Option>
))}
</Select>
</Row>
</div>
)
}
Example #13
Source File: PopupContents.tsx From firetable with Apache License 2.0 | 4 votes |
// TODO: Implement infinite scroll here
export default function PopupContents({
value = [],
onChange,
config,
row,
docRef,
}: IPopupContentsProps) {
const url = config.url;
const titleKey = config.titleKey ?? config.primaryKey;
const subtitleKey = config.subtitleKey;
const resultsKey = config.resultsKey;
const primaryKey = config.primaryKey;
const multiple = Boolean(config.multiple);
const classes = useStyles();
// Webservice search query
const [query, setQuery] = useState("");
// Webservice response
const [response, setResponse] = useState<any | null>(null);
const [docData, setDocData] = useState<any | null>(null);
useEffect(() => {
docRef.get().then((d) => setDocData(d.data()));
}, []);
const hits: any["hits"] = _get(response, resultsKey) ?? [];
const [search] = useDebouncedCallback(
async (query: string) => {
if (!docData) return;
if (!url) return;
const uri = new URL(url),
params = { q: query };
Object.keys(params).forEach((key) =>
uri.searchParams.append(key, params[key])
);
const resp = await fetch(uri.toString(), {
method: "POST",
body: JSON.stringify(docData),
headers: { "content-type": "application/json" },
});
const jsonBody = await resp.json();
setResponse(jsonBody);
},
1000,
{ leading: true }
);
useEffect(() => {
search(query);
}, [query, docData]);
if (!response) return <Loading />;
const select = (hit: any) => () => {
if (multiple) onChange([...value, hit]);
else onChange([hit]);
};
const deselect = (hit: any) => () => {
if (multiple)
onChange(value.filter((v) => v[primaryKey] !== hit[primaryKey]));
else onChange([]);
};
const selectedValues = value?.map((item) => _get(item, primaryKey));
const clearSelection = () => onChange([]);
return (
<Grid container direction="column" className={classes.grid}>
<Grid item className={classes.searchRow}>
<TextField
value={query}
onChange={(e) => setQuery(e.target.value)}
fullWidth
variant="filled"
margin="dense"
label="Search items"
className={classes.noMargins}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<SearchIcon />
</InputAdornment>
),
}}
onClick={(e) => e.stopPropagation()}
onKeyDown={(e) => e.stopPropagation()}
/>
</Grid>
<Grid item xs className={classes.listRow}>
<List className={classes.list}>
{hits.map((hit) => {
const isSelected =
selectedValues.indexOf(_get(hit, primaryKey)) !== -1;
console.log(`Selected Values: ${selectedValues}`);
return (
<React.Fragment key={_get(hit, primaryKey)}>
<MenuItem
dense
onClick={isSelected ? deselect(hit) : select(hit)}
>
<ListItemIcon className={classes.checkboxContainer}>
{multiple ? (
<Checkbox
edge="start"
checked={isSelected}
tabIndex={-1}
color="secondary"
className={classes.checkbox}
disableRipple
inputProps={{
"aria-labelledby": `label-${_get(hit, primaryKey)}`,
}}
/>
) : (
<Radio
edge="start"
checked={isSelected}
tabIndex={-1}
color="secondary"
className={classes.checkbox}
disableRipple
inputProps={{
"aria-labelledby": `label-${_get(hit, primaryKey)}`,
}}
/>
)}
</ListItemIcon>
<ListItemText
id={`label-${_get(hit, primaryKey)}`}
primary={_get(hit, titleKey)}
secondary={!subtitleKey ? "" : _get(hit, subtitleKey)}
/>
</MenuItem>
<Divider className={classes.divider} />
</React.Fragment>
);
})}
</List>
</Grid>
{multiple && (
<Grid item className={clsx(classes.footerRow, classes.selectedRow)}>
<Grid
container
direction="row"
justify="space-between"
alignItems="center"
>
<Typography
variant="button"
color="textSecondary"
className={classes.selectedNum}
>
{value?.length} of {hits?.length}
</Typography>
<Button
disabled={!value || value.length === 0}
onClick={clearSelection}
color="primary"
className={classes.selectAllButton}
>
Clear Selection
</Button>
</Grid>
</Grid>
)}
</Grid>
);
}
Example #14
Source File: index.tsx From jsonschema-editor-react with Apache License 2.0 | 4 votes |
SchemaItem: React.FunctionComponent<SchemaItemProps> = (
props: React.PropsWithChildren<SchemaItemProps>
) => {
const {
name,
itemStateProp,
showadvanced,
required,
parentStateProp,
isReadOnly,
} = props;
// const itemState = useState(itemStateProp);
const parentState = useState(parentStateProp);
const parentStateOrNull: State<JSONSchema7> | undefined = parentState.ornull;
const propertiesOrNull:
| State<{
[key: string]: JSONSchema7Definition;
}>
| undefined = parentStateOrNull.properties.ornull;
const nameState = useState(name);
const isReadOnlyState = useState(isReadOnly);
const itemState = useState(
(parentStateProp.properties as State<{
[key: string]: JSONSchema7;
}>).nested(nameState.value)
);
const { length } = parentState.path.filter((name) => name !== "properties");
const tagPaddingLeftStyle = {
paddingLeft: `${20 * (length + 1)}px`,
};
const isRequired = required
? required.length > 0 && required.includes(name)
: false;
const toast = useToast();
// Debounce callback
const debounced = useDebouncedCallback(
// function
(newValue: string) => {
// Todo: make toast for duplicate properties
if (propertiesOrNull && propertiesOrNull[newValue].value) {
toast({
title: "Duplicate Property",
description: "Property already exists!",
status: "error",
duration: 1000,
isClosable: true,
position: "top",
});
} else {
const oldName = name;
const proptoupdate = newValue;
const newobj = renameKeys(
{ [oldName]: proptoupdate },
parentState.properties.value
);
parentStateOrNull.properties.set(JSON.parse(JSON.stringify(newobj)));
}
},
// delay in ms
1000
);
if (!itemState.value) {
return <></>;
}
return (
<div>
<Flex
alignContent="space-evenly"
direction="row"
wrap="nowrap"
className="schema-item"
style={tagPaddingLeftStyle}
>
<Input
isDisabled={isReadOnlyState.value}
defaultValue={nameState.value}
size="sm"
margin={2}
variant="outline"
placeholder="Enter property name"
onChange={(evt: React.ChangeEvent<HTMLInputElement>) => {
debounced(evt.target.value);
}}
/>
<Checkbox
isDisabled={isReadOnlyState.value}
isChecked={isRequired}
margin={2}
colorScheme="blue"
onChange={(evt: React.ChangeEvent<HTMLInputElement>) => {
if (!evt.target.checked && required.includes(name)) {
(parentState.required as State<string[]>)[
required.indexOf(name)
].set(none);
} else {
parentState.required.merge([name]);
}
}}
/>
<Select
isDisabled={false}
variant="outline"
value={itemState.type.value}
size="sm"
margin={2}
placeholder="Choose data type"
onChange={(evt: React.ChangeEvent<HTMLSelectElement>) => {
const newSchema = handleTypeChange(
evt.target.value as JSONSchema7TypeName,
false
);
itemState.set(newSchema as JSONSchema7);
}}
>
{SchemaTypes.map((item, index) => {
return (
<option key={String(index)} value={item}>
{item}
</option>
);
})}
</Select>
<Input
isDisabled={isReadOnlyState.value}
value={itemState.title.value || ""}
size="sm"
margin={2}
variant="outline"
placeholder="Add Title"
onChange={(evt: React.ChangeEvent<HTMLInputElement>) => {
itemState.title.set(evt.target.value);
}}
/>
<Input
isDisabled={isReadOnlyState.value}
value={itemState.description.value || ""}
size="sm"
margin={2}
variant="outline"
placeholder="Add Description"
onChange={(evt: React.ChangeEvent<HTMLInputElement>) => {
itemState.description.set(evt.target.value);
}}
/>
{itemState.type.value !== "object" && itemState.type.value !== "array" && (
<Tooltip
hasArrow
aria-label="Advanced Settings"
label="Advanced Settings"
placement="top"
>
<IconButton
isRound
isDisabled={isReadOnlyState.value}
size="sm"
mt={2}
mb={2}
ml={1}
variant="link"
colorScheme="blue"
fontSize="16px"
icon={<FiSettings />}
aria-label="Advanced Settings"
onClick={() => {
showadvanced(name);
}}
/>
</Tooltip>
)}
<Tooltip
hasArrow
aria-label="Remove Node"
label="Remove Node"
placement="top"
>
<IconButton
isRound
isDisabled={isReadOnlyState.value}
size="sm"
mt={2}
mb={2}
ml={1}
variant="link"
colorScheme="red"
fontSize="16px"
icon={<AiOutlineDelete />}
aria-label="Remove Node"
onClick={() => {
const updatedState = deleteKey(
nameState.value,
JSON.parse(JSON.stringify(parentState.properties.value))
);
parentState.properties.set(updatedState);
}}
/>
</Tooltip>
{itemState.type?.value === "object" ? (
<DropPlus
isDisabled={isReadOnlyState.value}
parentStateProp={parentState}
itemStateProp={itemStateProp}
/>
) : (
<Tooltip
hasArrow
aria-label="Add Sibling Node"
label="Add Sibling Node"
placement="top"
>
<IconButton
isRound
isDisabled={isReadOnlyState.value}
size="sm"
mt={2}
mb={2}
mr={2}
variant="link"
colorScheme="green"
fontSize="16px"
icon={<IoIosAddCircleOutline />}
aria-label="Add Sibling Node"
onClick={() => {
if (propertiesOrNull) {
const fieldName = `field_${random()}`;
propertiesOrNull
?.nested(fieldName)
.set(getDefaultSchema(DataType.string) as JSONSchema7);
}
}}
/>
</Tooltip>
)}
</Flex>
{itemState.type?.value === "object" && (
<SchemaObject isReadOnly={isReadOnlyState} schemaState={itemState} />
)}
{itemState.type?.value === "array" && (
<SchemaArray isReadOnly={isReadOnlyState} schemaState={itemState} />
)}
</div>
);
}
Example #15
Source File: SessionItemComponent.tsx From caritas-onlineBeratung-frontend with GNU Affero General Public License v3.0 | 4 votes |
SessionItemComponent = (props: SessionItemProps) => {
const { rcGroupId: groupIdFromParam } = useParams();
const activeSession = useContext(ActiveSessionContext);
let { sessionsData, setSessionsData } = useContext(SessionsDataContext);
const { userData } = useContext(UserDataContext);
const [monitoringButtonVisible, setMonitoringButtonVisible] =
useState(false);
const [overlayItem, setOverlayItem] = useState<OverlayItem>(null);
const [currentGroupId, setCurrentGroupId] = useState(null);
const { setAcceptedGroupId } = useContext(AcceptedGroupIdContext);
const chatItem = getChatItemForSession(
activeSession
) as SessionItemInterface;
const isGroupChat = isGroupChatForSessionItem(activeSession);
const isLiveChat = isAnonymousSession(activeSession?.session);
const messages = useMemo(() => props.messages, [props && props.messages]); // eslint-disable-line react-hooks/exhaustive-deps
const [initialScrollCompleted, setInitialScrollCompleted] = useState(false);
const [isRequestInProgress, setIsRequestInProgress] = useState(false);
const scrollContainerRef = React.useRef<HTMLDivElement>(null);
const [isScrolledToBottom, setIsScrolledToBottom] = useState(true);
const [newMessages, setNewMessages] = useState(0);
const { setUpdateSessionList } = useContext(UpdateSessionListContext);
const [sessionListTab] = useState(
new URLSearchParams(useLocation().search).get('sessionListTab')
);
const getSessionListTab = () =>
`${sessionListTab ? `?sessionListTab=${sessionListTab}` : ''}`;
const { isAnonymousEnquiry } = props;
const resetUnreadCount = () => {
if (!isAnonymousEnquiry) {
setNewMessages(0);
initMessageCount = messages?.length;
scrollContainerRef.current
.querySelectorAll('.messageItem__divider--lastRead')
.forEach((e) => e.remove());
}
};
useEffect(() => {
if (scrollContainerRef.current) {
resetUnreadCount();
}
}, [scrollContainerRef]); // eslint-disable-line
useEffect(() => {
if (!isAnonymousEnquiry && messages) {
if (
initialScrollCompleted &&
isMyMessage(messages[messages.length - 1]?.userId)
) {
resetUnreadCount();
scrollToEnd(0, true);
} else {
// if first unread message -> prepend element
if (newMessages === 0 && !isScrolledToBottom) {
const scrollContainer = scrollContainerRef.current;
const firstUnreadItem = Array.from(
scrollContainer.querySelectorAll('.messageItem')
).pop();
const lastReadDivider = document.createElement('div');
lastReadDivider.innerHTML = translate(
'session.divider.lastRead'
);
lastReadDivider.className =
'messageItem__divider messageItem__divider--lastRead';
firstUnreadItem.prepend(lastReadDivider);
}
if (isScrolledToBottom && initialScrollCompleted) {
resetUnreadCount();
scrollToEnd(0, true);
}
setNewMessages(messages.length - initMessageCount);
}
}
}, [messages?.length]); // eslint-disable-line
useEffect(() => {
if (isScrolledToBottom) {
resetUnreadCount();
}
}, [isScrolledToBottom]); // eslint-disable-line
const [resortData, setResortData] = useState<ConsultingTypeInterface>();
useEffect(() => {
let isCanceled = false;
apiGetConsultingType({
consultingTypeId: chatItem?.consultingType
}).then((response) => {
if (isCanceled) return;
setResortData(response);
});
return () => {
isCanceled = true;
};
}, [chatItem?.consultingType]);
const getPlaceholder = () => {
if (isGroupChat) {
return translate('enquiry.write.input.placeholder.groupChat');
} else if (hasUserAuthority(AUTHORITIES.ASKER_DEFAULT, userData)) {
return translate('enquiry.write.input.placeholder');
} else if (
hasUserAuthority(AUTHORITIES.VIEW_ALL_PEER_SESSIONS, userData) &&
activeSession.isFeedbackSession
) {
return translate('enquiry.write.input.placeholder.feedback.main');
} else if (
hasUserAuthority(AUTHORITIES.CONSULTANT_DEFAULT, userData) &&
activeSession.isFeedbackSession
) {
return translate('enquiry.write.input.placeholder.feedback.peer');
} else if (hasUserAuthority(AUTHORITIES.CONSULTANT_DEFAULT, userData)) {
return translate('enquiry.write.input.placeholder.consultant');
}
return translate('enquiry.write.input.placeholder');
};
const handleButtonClick = (sessionId: any, sessionGroupId: string) => {
if (isRequestInProgress) {
return null;
}
setIsRequestInProgress(true);
apiEnquiryAcceptance(sessionId, isAnonymousEnquiry)
.then(() => {
setOverlayItem(enquirySuccessfullyAcceptedOverlayItem);
setCurrentGroupId(sessionGroupId);
})
.catch((error) => {
if (error.message === FETCH_ERRORS.CONFLICT) {
setOverlayItem(enquiryTakenByOtherConsultantOverlayItem);
} else {
console.log(error);
}
});
};
const handleOverlayAction = (buttonFunction: string) => {
switch (buttonFunction) {
case OVERLAY_FUNCTIONS.REDIRECT:
setOverlayItem(null);
setIsRequestInProgress(false);
setAcceptedGroupId(currentGroupId);
setSessionsData({ ...sessionsData, enquiries: [] });
history.push(`/sessions/consultant/sessionView/`);
break;
case OVERLAY_FUNCTIONS.CLOSE:
setOverlayItem(null);
setUpdateSessionList(SESSION_LIST_TYPES.ENQUIRY);
history.push(
`/sessions/consultant/sessionPreview${getSessionListTab()}`
);
break;
default:
// Should never be executed as `handleOverlayAction` is only called
// with a non-null `overlayItem`
}
};
/* eslint-disable */
const handleScroll = useDebouncedCallback((e) => {
const scrollPosition = Math.round(
e.target.scrollHeight - e.target.scrollTop
);
const containerHeight = e.target.clientHeight;
const isBottom =
scrollPosition >= containerHeight - 1 &&
scrollPosition <= containerHeight + 1;
setIsScrolledToBottom(isBottom);
}, 100);
/* eslint-enable */
const handleScrollToBottomButtonClick = () => {
if (newMessages > 0) {
const scrollContainer = scrollContainerRef.current;
const sessionHeader =
scrollContainer.parentElement.getElementsByClassName(
'sessionInfo'
)[0] as HTMLElement;
const messageItems = scrollContainer.querySelectorAll(
'.messageItem:not(.messageItem--right)'
);
const firstUnreadItem = messageItems[
messageItems.length - newMessages
] as HTMLElement;
const firstUnreadItemOffet =
firstUnreadItem.offsetTop - sessionHeader.offsetHeight;
if (scrollContainer.scrollTop < firstUnreadItemOffet) {
smoothScroll({
duration: 1000,
element: scrollContainer,
to: firstUnreadItemOffet
});
} else {
scrollToEnd(0, true);
}
} else {
scrollToEnd(0, true);
}
};
const enableInitialScroll = () => {
if (!initialScrollCompleted) {
setInitialScrollCompleted(true);
scrollToEnd(500, true);
}
};
const isOnlyEnquiry = typeIsEnquiry(getTypeOfLocation());
const buttonItem: ButtonItem = {
label: isAnonymousEnquiry
? translate('enquiry.acceptButton.anonymous')
: translate('enquiry.acceptButton'),
type: BUTTON_TYPES.PRIMARY
};
const enquirySuccessfullyAcceptedOverlayItem: OverlayItem = {
svg: CheckIcon,
headline: translate('session.acceptance.overlayHeadline'),
buttonSet: [
{
label: translate('session.acceptance.buttonLabel'),
function: OVERLAY_FUNCTIONS.REDIRECT,
type: BUTTON_TYPES.PRIMARY
}
]
};
const enquiryTakenByOtherConsultantOverlayItem: OverlayItem = {
svg: XIcon,
headline: translate(
'session.anonymous.takenByOtherConsultant.overlayHeadline'
),
illustrationBackground: 'error',
buttonSet: [
{
label: translate(
'session.anonymous.takenByOtherConsultant.buttonLabel'
),
function: OVERLAY_FUNCTIONS.CLOSE,
type: BUTTON_TYPES.PRIMARY
}
]
};
const monitoringButtonItem: ButtonItem = {
label: translate('session.monitoring.buttonLabel'),
type: 'PRIMARY',
function: ''
};
const scrollBottomButtonItem: ButtonItem = {
icon: <ArrowDoubleDownIcon />,
type: BUTTON_TYPES.SMALL_ICON,
smallIconBackgroundColor: 'alternate'
};
return (
<div
className={
activeSession.isFeedbackSession
? `session session--yellowTheme`
: `session`
}
>
<SessionHeaderComponent
consultantAbsent={
activeSession.consultant && activeSession.consultant.absent
? activeSession.consultant
: null
}
hasUserInitiatedStopOrLeaveRequest={
props.hasUserInitiatedStopOrLeaveRequest
}
legalComponent={props.legalComponent}
bannedUsers={props.bannedUsers}
/>
{!isAnonymousEnquiry && (
<div
id="session-scroll-container"
className="session__content"
ref={scrollContainerRef}
onScroll={(e) => handleScroll(e)}
>
{messages &&
resortData &&
messages.map((message: MessageItem, index) => (
<React.Fragment key={index}>
<MessageItemComponent
clientName={
getContact(activeSession).username
}
askerRcId={chatItem.askerRcId}
type={getTypeOfLocation()}
isOnlyEnquiry={isOnlyEnquiry}
isMyMessage={isMyMessage(message.userId)}
resortData={resortData}
bannedUsers={props.bannedUsers}
{...message}
/>
{index === messages.length - 1 &&
enableInitialScroll()}
</React.Fragment>
))}
<div
className={`session__scrollToBottom ${
isScrolledToBottom
? 'session__scrollToBottom--disabled'
: ''
}`}
>
{newMessages > 0 && (
<span className="session__unreadCount">
{newMessages > 99
? translate('session.unreadCount.maxValue')
: newMessages}
</span>
)}
<Button
item={scrollBottomButtonItem}
isLink={false}
buttonHandle={handleScrollToBottomButtonClick}
/>
</div>
</div>
)}
{isAnonymousEnquiry && (
<div className="session__content session__content--anonymousEnquiry">
<Headline
semanticLevel="3"
text={`${translate(
'enquiry.anonymous.infoLabel.start'
)}${getContact(activeSession).username}${translate(
'enquiry.anonymous.infoLabel.end'
)}`}
/>
</div>
)}
{chatItem.monitoring &&
!hasUserAuthority(AUTHORITIES.ASKER_DEFAULT, userData) &&
!activeSession.isFeedbackSession &&
!typeIsEnquiry(getTypeOfLocation()) &&
monitoringButtonVisible &&
!isLiveChat && (
<Link
to={`/sessions/consultant/${getViewPathForType(
getTypeOfLocation()
)}/${chatItem.groupId}/${
chatItem.id
}/userProfile/monitoring${getSessionListTab()}`}
>
<div className="monitoringButton">
<Button item={monitoringButtonItem} isLink={true} />
</div>
</Link>
)}
{typeIsEnquiry(getTypeOfLocation()) ? (
<div className="session__acceptance messageItem">
{!isLiveChat &&
hasUserAuthority(
AUTHORITIES.VIEW_ALL_PEER_SESSIONS,
userData
) ? (
<SessionAssign />
) : (
<Button
item={buttonItem}
buttonHandle={() =>
handleButtonClick(chatItem.id, chatItem.groupId)
}
/>
)}
</div>
) : null}
{!isAnonymousEnquiry &&
(!typeIsEnquiry(getTypeOfLocation()) ||
(typeIsEnquiry(getTypeOfLocation()) &&
hasUserAuthority(
AUTHORITIES.VIEW_ALL_PEER_SESSIONS,
userData
))) && (
<MessageSubmitInterfaceComponent
handleSendButton={() => {}}
isTyping={props.isTyping}
className={clsx(
'session__submit-interface',
!isScrolledToBottom &&
'session__submit-interface--scrolled-up'
)}
placeholder={getPlaceholder()}
showMonitoringButton={() => {
setMonitoringButtonVisible(true);
}}
type={getTypeOfLocation()}
typingUsers={props.typingUsers}
groupIdFromParam={groupIdFromParam}
/>
)}
{overlayItem && (
<OverlayWrapper>
<Overlay
item={overlayItem}
handleOverlay={handleOverlayAction}
/>
</OverlayWrapper>
)}
</div>
);
}
Example #16
Source File: useTableConfig.ts From firetable with Apache License 2.0 | 4 votes |
useTableConfig = (tablePath?: string) => {
const [tableConfigState, documentDispatch] = useDoc({
path: tablePath ? formatPath(tablePath) : "",
});
useEffect(() => {
const { doc, columns, rowHeight } = tableConfigState;
// TODO: REMOVE THIS
// Copy columns, rowHeight to tableConfigState
if (doc && columns !== doc.columns) {
documentDispatch({ columns: doc.columns });
}
if (doc && rowHeight !== doc.rowHeight) {
documentDispatch({ rowHeight: doc.rowHeight });
}
}, [tableConfigState.doc]);
/** used for specifying the table in use
* @param table firestore collection path
*/
const setTable = (table: string) => {
documentDispatch({
path: formatPath(table),
columns: [],
doc: null,
ref: db.doc(formatPath(table)),
loading: true,
});
};
/** used for creating a new column
* @param name of column.
* @param type of column
* @param data additional column properties
*/
const addColumn = (name: string, type: FieldType, data?: any) => {
//TODO: validation
const { columns } = tableConfigState;
const newIndex = Object.keys(columns).length ?? 0;
let updatedColumns = { ...columns };
const key = _camelCase(name);
updatedColumns[key] = { name, key, type, ...data, index: newIndex ?? 0 };
documentDispatch({
action: DocActions.update,
data: { columns: updatedColumns },
});
};
/** used for updating the width of column
* @param index of column.
* @param width number of pixels, eg: 120
*/
const [resize] = useDebouncedCallback((index: number, width: number) => {
const { columns } = tableConfigState;
const numberOfFixedColumns = Object.values(columns).filter(
(col: any) => col.fixed && !col.hidden
).length;
const columnsArray = _sortBy(
Object.values(columns).filter((col: any) => !col.hidden && !col.fixed),
"index"
);
let column: any = columnsArray[index - numberOfFixedColumns];
column.width = width;
let updatedColumns = columns;
updatedColumns[column.key] = column;
documentDispatch({
action: DocActions.update,
data: { columns: updatedColumns },
});
}, 1000);
type updatable = { field: string; value: unknown };
/** used for updating column properties such as type,name etc.
* @param index of column.
* @param {updatable[]} updatables properties to be updated
*/
const updateColumn = (key: string, updates: any) => {
const { columns } = tableConfigState;
const updatedColumns = {
...columns,
[key]: { ...columns[key], ...updates },
};
documentDispatch({
action: DocActions.update,
data: { columns: updatedColumns },
});
};
/** remove column by index
* @param index of column.
*/
const remove = (key: string) => {
const { columns } = tableConfigState;
let updatedColumns = columns;
updatedColumns[key] = deleteField();
documentDispatch({
action: DocActions.update,
data: { columns: updatedColumns },
});
};
/** reorder columns by key
* @param draggedColumnKey column being repositioned.
* @param droppedColumnKey column being .
*/
const reorder = (draggedColumnKey: string, droppedColumnKey: string) => {
const { columns } = tableConfigState;
const oldIndex = columns[draggedColumnKey].index;
const newIndex = columns[droppedColumnKey].index;
const columnsArray = _sortBy(Object.values(columns), "index");
arrayMover(columnsArray, oldIndex, newIndex);
let updatedColumns = columns;
columnsArray
.filter((c) => c) // arrayMover has a bug creating undefined items
.forEach((column: any, index) => {
updatedColumns[column.key] = { ...column, index };
});
documentDispatch({
action: DocActions.update,
data: { columns: updatedColumns },
});
};
/** changing table configuration used for things such as row height
* @param key name of parameter eg. rowHeight
* @param value new value eg. 65
*/
const updateConfig = (key: string, value: unknown) => {
documentDispatch({
action: DocActions.update,
data: { [key]: value },
});
};
const actions = {
updateColumn,
updateConfig,
addColumn,
resize,
setTable,
remove,
reorder,
};
return [tableConfigState, actions];
}
Example #17
Source File: PopupContents.tsx From firetable with Apache License 2.0 | 4 votes |
// TODO: Implement infinite scroll here
export default function PopupContents({
value = [],
onChange,
config,
docRef,
}: IPopupContentsProps) {
const url = config.url;
const titleKey = config.titleKey ?? config.primaryKey;
const subtitleKey = config.subtitleKey;
const resultsKey = config.resultsKey;
const primaryKey = config.primaryKey;
const multiple = Boolean(config.multiple);
const classes = useStyles();
// Webservice search query
const [query, setQuery] = useState("");
// Webservice response
const [response, setResponse] = useState<any | null>(null);
const [docData, setDocData] = useState<any | null>(null);
useEffect(() => {
docRef.get().then((d) => setDocData(d.data()));
}, []);
const hits: any["hits"] = _get(response, resultsKey) ?? [];
const [search] = useDebouncedCallback(
async (query: string) => {
if (!docData) return;
if (!url) return;
const uri = new URL(url),
params = { q: query };
Object.keys(params).forEach((key) =>
uri.searchParams.append(key, params[key])
);
const resp = await fetch(uri.toString(), {
method: "POST",
body: JSON.stringify(docData),
headers: { "content-type": "application/json" },
});
const jsonBody = await resp.json();
setResponse(jsonBody);
},
1000,
{ leading: true }
);
useEffect(() => {
search(query);
}, [query, docData]);
if (!response) return <Loading />;
const select = (hit: any) => () => {
if (multiple) onChange([...value, hit]);
else onChange([hit]);
};
const deselect = (hit: any) => () => {
if (multiple)
onChange(value.filter((v) => v[primaryKey] !== hit[primaryKey]));
else onChange([]);
};
const selectedValues = value?.map((item) => _get(item, primaryKey));
const clearSelection = () => onChange([]);
return (
<Grid container direction="column" className={classes.grid}>
<Grid item className={classes.searchRow}>
<TextField
value={query}
onChange={(e) => setQuery(e.target.value)}
fullWidth
variant="filled"
margin="dense"
label="Search items"
className={classes.noMargins}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<SearchIcon />
</InputAdornment>
),
}}
onClick={(e) => e.stopPropagation()}
onKeyDown={(e) => e.stopPropagation()}
/>
</Grid>
<Grid item xs className={classes.listRow}>
<List className={classes.list}>
{hits.map((hit) => {
const isSelected =
selectedValues.indexOf(_get(hit, primaryKey)) !== -1;
return (
<React.Fragment key={_get(hit, primaryKey)}>
<MenuItem
dense
onClick={isSelected ? deselect(hit) : select(hit)}
>
<ListItemIcon className={classes.checkboxContainer}>
{multiple ? (
<Checkbox
edge="start"
checked={isSelected}
tabIndex={-1}
color="secondary"
className={classes.checkbox}
disableRipple
inputProps={{
"aria-labelledby": `label-${_get(hit, primaryKey)}`,
}}
/>
) : (
<Radio
edge="start"
checked={isSelected}
tabIndex={-1}
color="secondary"
className={classes.checkbox}
disableRipple
inputProps={{
"aria-labelledby": `label-${_get(hit, primaryKey)}`,
}}
/>
)}
</ListItemIcon>
<ListItemText
id={`label-${_get(hit, primaryKey)}`}
primary={_get(hit, titleKey)}
secondary={!subtitleKey ? "" : _get(hit, subtitleKey)}
/>
</MenuItem>
<Divider className={classes.divider} />
</React.Fragment>
);
})}
</List>
</Grid>
{multiple && (
<Grid item className={clsx(classes.footerRow, classes.selectedRow)}>
<Grid
container
direction="row"
justify="space-between"
alignItems="center"
>
<Typography
variant="button"
color="textSecondary"
className={classes.selectedNum}
>
{value?.length} of {hits?.length}
</Typography>
<Button
disabled={!value || value.length === 0}
onClick={clearSelection}
color="primary"
className={classes.selectAllButton}
>
Clear Selection
</Button>
</Grid>
</Grid>
)}
</Grid>
);
}
Example #18
Source File: ImportCsv.tsx From firetable with Apache License 2.0 | 4 votes |
export default function ImportCsv({ render, PopoverProps }: IImportCsvProps) {
const classes = useStyles();
const [open, setOpen] = useState<HTMLButtonElement | null>(null);
const [tab, setTab] = useState("upload");
const [csvData, setCsvData] = useState<IImportCsvWizardProps["csvData"]>(
null
);
const [error, setError] = useState("");
const validCsv =
csvData !== null && csvData?.columns.length > 0 && csvData?.rows.length > 0;
const handleOpen = (event: React.MouseEvent<HTMLButtonElement>) =>
setOpen(event.currentTarget);
const handleClose = () => {
setOpen(null);
setCsvData(null);
setTab("upload");
setError("");
};
const popoverId = open ? "csv-popover" : undefined;
const parseCsv = (csvString: string) =>
parse(csvString, {}, (err, rows) => {
if (err) {
setError(err.message);
} else {
const columns = rows.shift() ?? [];
if (columns.length === 0) {
setError("No columns detected");
} else {
const mappedRows = rows.map((row) =>
row.reduce((a, c, i) => ({ ...a, [columns[i]]: c }), {})
);
setCsvData({ columns, rows: mappedRows });
setError("");
}
}
});
const onDrop = useCallback(async (acceptedFiles) => {
const file = acceptedFiles[0];
const reader = new FileReader();
reader.onload = (event: any) => parseCsv(event.target.result);
reader.readAsText(file);
}, []);
const { getRootProps, getInputProps, isDragActive } = useDropzone({
onDrop,
multiple: false,
accept: "text/csv",
});
const [handlePaste] = useDebouncedCallback(
(value: string) => parseCsv(value),
1000
);
const [loading, setLoading] = useState(false);
const [handleUrl] = useDebouncedCallback((value: string) => {
setLoading(true);
setError("");
fetch(value, { mode: "no-cors" })
.then((res) => res.text())
.then((data) => {
parseCsv(data);
setLoading(false);
})
.catch((e) => {
setError(e.message);
setLoading(false);
});
}, 1000);
const [openWizard, setOpenWizard] = useState(false);
return (
<>
{render ? (
render(handleOpen)
) : (
<TableHeaderButton
title="Import CSV"
onClick={handleOpen}
icon={<ImportIcon />}
/>
)}
<Popover
id={popoverId}
open={!!open}
anchorEl={open}
onClose={handleClose}
anchorOrigin={{
vertical: "bottom",
horizontal: "right",
}}
transformOrigin={{
vertical: "top",
horizontal: "right",
}}
{...PopoverProps}
>
<TabContext value={tab}>
<TabList
onChange={(_, v) => {
setTab(v);
setCsvData(null);
setError("");
}}
aria-label="Import CSV method tabs"
action={(actions) =>
setTimeout(() => actions?.updateIndicator(), 200)
}
>
<Tab label="Upload" value="upload" />
<Tab label="Paste" value="paste" />
<Tab label="URL" value="url" />
</TabList>
<TabPanel value="upload" className={classes.tabPanel}>
<Grid
container
justify="center"
alignContent="center"
alignItems="center"
direction="column"
{...getRootProps()}
className={clsx(classes.dropzone, error && classes.error)}
>
<input {...getInputProps()} />
{isDragActive ? (
<Typography variant="overline">Drop CSV file here…</Typography>
) : (
<>
<Grid item>
{validCsv ? (
<CheckIcon color="inherit" />
) : (
<FileUploadIcon color="inherit" />
)}
</Grid>
<Grid item>
<Typography variant="overline" color="inherit">
{validCsv
? "Valid CSV"
: "Click to upload or drop CSV file here"}
</Typography>
</Grid>
</>
)}
</Grid>
{error && (
<FormHelperText error className={classes.dropzoneError}>
{error}
</FormHelperText>
)}
</TabPanel>
<TabPanel value="paste" className={classes.tabPanel}>
<TextField
variant="filled"
multiline
inputProps={{ minRows: 5 }}
autoFocus
fullWidth
label="Paste your CSV here"
onChange={(e) => {
if (csvData !== null) setCsvData(null);
handlePaste(e.target.value);
}}
InputProps={{
classes: {
root: classes.pasteField,
input: classes.pasteInput,
},
}}
helperText={error}
error={!!error}
/>
</TabPanel>
<TabPanel value="url" className={classes.tabPanel}>
<TextField
variant="filled"
autoFocus
fullWidth
label="Paste the link to the CSV file here"
onChange={(e) => {
if (csvData !== null) setCsvData(null);
handleUrl(e.target.value);
}}
helperText={loading ? "Fetching CSV…" : error}
error={!!error}
/>
</TabPanel>
</TabContext>
<Button
endIcon={<GoIcon />}
disabled={!validCsv}
className={classes.continueButton}
onClick={() => setOpenWizard(true)}
>
Continue
</Button>
</Popover>
{openWizard && csvData && (
<ImportCsvWizard
handleClose={() => setOpenWizard(false)}
csvData={csvData}
/>
)}
</>
);
}
Example #19
Source File: AlgoliaFilters.tsx From firetable with Apache License 2.0 | 4 votes |
export default function AlgoliaFilters({
index,
request,
requestDispatch,
requiredFilters,
label,
filters,
search = true,
}: IAlgoliaFiltersProps) {
const classes = useStyles();
// Store filter values
const [filterValues, setFilterValues] = useState<Record<string, string[]>>(
{}
);
// Push filter values to dispatch
useEffect(() => {
const filtersString = generateFiltersString(filterValues, requiredFilters);
if (filtersString === null) return;
requestDispatch({ filters: filtersString });
}, [filterValues]);
// Store facet values
const [facetValues, setFacetValues] = useState<
Record<string, readonly FacetHit[]>
>({});
// Get facet values
useEffect(() => {
if (!index) return;
filters.forEach((filter) => {
const params = { ...request, maxFacetHits: 100 };
// Ignore current user-selected value for these filters so all options
// continue to show up
params.filters =
generateFiltersString(
{ ...filterValues, [filter.facet]: [] },
requiredFilters
) ?? "";
index
.searchForFacetValues(filter.facet, "", params)
.then(({ facetHits }) =>
setFacetValues((other) => ({ ...other, [filter.facet]: facetHits }))
);
});
}, [filters, index, filterValues, requiredFilters]);
// Reset filters
const handleResetFilters = () => {
setFilterValues({});
setQuery("");
requestDispatch({ filters: requiredFilters ?? "", query: "" });
};
// Store search query
const [query, setQuery] = useState("");
const [handleQueryChange] = useDebouncedCallback(
(query: string) => requestDispatch({ query }),
500
);
return (
<div>
<Grid container spacing={1} alignItems="center">
<Grid item xs>
<Typography variant="overline">
Filter{label ? " " + label : "s"}
</Typography>
</Grid>
<Grid item>
<Button
color="primary"
onClick={handleResetFilters}
className={classes.resetFilters}
disabled={query === "" && Object.keys(filterValues).length === 0}
>
Reset Filters
</Button>
</Grid>
</Grid>
<Grid
container
spacing={2}
alignItems="center"
className={classes.filterGrid}
>
{search && (
<Grid item xs={12} md={4} lg={3}>
<TextField
value={query}
onChange={(e) => {
setQuery(e.target.value);
handleQueryChange(e.target.value);
}}
variant="filled"
type="search"
InputProps={{
startAdornment: (
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
),
}}
aria-label={`Search${label ? " " + label : ""}`}
placeholder={`Search${label ? " " + label : ""}`}
hiddenLabel
fullWidth
/>
</Grid>
)}
{filters.map((filter) => (
<Grid item key={filter.facet} xs={12} sm={6} md={4} lg={3}>
<MultiSelect
label={filter.label}
value={filterValues[filter.facet] ?? []}
onChange={(value) =>
setFilterValues((other) => ({
...other,
[filter.facet]: value,
}))
}
options={
facetValues[filter.facet]?.map((item) => ({
value: item.value,
label: filter.labelTransformer
? filter.labelTransformer(item.value)
: item.value,
count: item.count,
})) ?? []
}
itemRenderer={(option) => (
<React.Fragment key={option.value}>
{option.label}
<ListItemSecondaryAction className={classes.count}>
<Typography
variant="body2"
color="inherit"
component="span"
>
{(option as any).count}
</Typography>
</ListItemSecondaryAction>
</React.Fragment>
)}
searchable={facetValues[filter.facet]?.length > 10}
/>
</Grid>
))}
</Grid>
</div>
);
}