Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions frontend/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -143,3 +143,22 @@ div[data-tippy-root] {
border-color: var(--color-gray-200, currentcolor);
}
}

/* react-datepicker inside InputDate */
.conquery-datepicker {
.react-datepicker-wrapper {
position: absolute;
top: 0;
bottom: 0;
z-index: -1;
}
.react-datepicker-popper[data-placement^="bottom"] {
padding-top: 4px;
}
.react-datepicker-popper[data-placement^="top"] {
padding-bottom: 0;
}
.react-datepicker__day--selected {
background: var(--color-primary-500);
}
}
112 changes: 52 additions & 60 deletions frontend/src/js/ui-components/BaseInput.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import styled from "@emotion/styled";
import {
faCheck,
faExclamationTriangle,
Expand All @@ -11,6 +10,7 @@ import {
useCallback,
} from "react";
import { useTranslation } from "react-i18next";
import { tv } from "tailwind-variants";

import type { CurrencyConfigT } from "../api/types";
import IconButton from "../button/IconButton";
Expand All @@ -21,56 +21,41 @@ import WithTooltip from "../tooltip/WithTooltip";

import CurrencyInput from "./CurrencyInput";

const Root = styled("div")`
position: relative;
`;

const Input = styled("input")<{ large?: boolean; disabled?: boolean }>`
outline: 0;
width: 100%;
opacity: ${({ disabled }) => (disabled ? 0.5 : 1)};

border: 1px solid ${({ theme }) => theme.col.grayMediumLight};
padding: ${({ large }) =>
large ? "10px 30px 10px 14px" : "6px 30px 6px 10px"};
font-size: ${({ theme, large }) => (large ? theme.font.lg : theme.font.sm)};
border-radius: ${({ theme }) => theme.borderRadius};
font-weight: 400;
`;

const SignalIcon = styled(FaIcon)`
position: absolute;
top: 8px;
right: 35px;
opacity: 0.8;
`;

const GreenIcon = styled(SignalIcon)`
color: ${({ theme }) => theme.col.green};
`;
const RedIcon = styled(FaIcon)`
color: ${({ theme }) => theme.col.red};
opacity: 0.8;
`;
const AbsoluteWrap = styled("div")`
position: absolute;
top: 5px;
right: 35px;
`;

const ClearZoneIconButton = styled(IconButton)`
position: absolute;
top: 0;
right: 10px;
cursor: pointer;
height: 100%;
display: flex;
align-items: center;

&:hover {
color: ${({ theme }) => theme.col.red};
}
`;
const root = tv({ base: "relative" });

const input = tv({
base: [
"outline-0",
"w-full",
"border border-gray-400",
"rounded",
"py-[6px] pr-[30px] pl-[10px]",
"text-sm",
"font-normal",
],
variants: {
large: { true: "py-[10px] pr-[30px] pl-[14px] text-xl" },
disabled: { true: "opacity-50" },
},
});

const greenIcon = tv({
base: ["absolute top-2 right-[35px]", "opacity-80", "text-green"],
});

const redIcon = tv({ base: ["opacity-80", "text-red"] });

const absoluteWrap = tv({ base: "absolute top-[5px] right-[35px]" });

const clearZoneIconButton = tv({
base: [
"absolute top-0 right-[10px]",
"h-full",
"flex items-center",
"cursor-pointer",
"hover:text-red",
],
});

interface InputProps {
autoFocus?: boolean;
Expand Down Expand Up @@ -160,7 +145,7 @@ const BaseInput = ({
const isCurrencyInput = money && !!currencyConfig;

return (
<Root className={className}>
<div className={root({ className })}>
{isCurrencyInput ? (
<CurrencyInput
currencyConfig={currencyConfig}
Expand All @@ -170,7 +155,8 @@ const BaseInput = ({
onChange={safeOnChange}
/>
) : (
<Input
<input
className={input({ large, disabled })}
placeholder={placeholder}
type={inputType}
ref={ref}
Expand All @@ -184,7 +170,6 @@ const BaseInput = ({
safeOnChange(value);
}}
value={exists(value) ? value : ""}
large={large}
disabled={disabled}
onFocus={onFocus}
onBlur={onBlur}
Expand All @@ -201,15 +186,22 @@ const BaseInput = ({
)}
{exists(value) && !isEmpty(value) && (
<>
{valid && !invalid && <GreenIcon icon={faCheck} large={large} />}
{valid && !invalid && (
<FaIcon icon={faCheck} large={large} className={greenIcon()} />
)}
{invalid && (
<WithTooltip text={invalidText}>
<AbsoluteWrap>
<RedIcon icon={faExclamationTriangle} large={large} />
</AbsoluteWrap>
<div className={absoluteWrap()}>
<FaIcon
icon={faExclamationTriangle}
large={large}
className={redIcon()}
/>
</div>
</WithTooltip>
)}
<ClearZoneIconButton
<IconButton
className={clearZoneIconButton()}
tiny
icon={faTimes}
tabIndex={-1}
Expand All @@ -220,7 +212,7 @@ const BaseInput = ({
/>
</>
)}
</Root>
</div>
);
};

Expand Down
72 changes: 40 additions & 32 deletions frontend/src/js/ui-components/CurrencyInput.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,28 @@
import styled from "@emotion/styled";
import { useEffect, useState } from "react";
import { NumericFormat } from "react-number-format";
import {
type InputAttributes,
NumericFormat,
type NumericFormatProps,
} from "react-number-format";
import { tv } from "tailwind-variants";

import type { CurrencyConfigT } from "../api/types";
import { isEmpty } from "../common/helpers/commonHelper";
import { exists } from "../common/helpers/exists";

const SxNumberFormat = styled(NumericFormat)<{ large?: boolean }>`
outline: 0;
min-width: 170px;

border: 1px solid ${({ theme }) => theme.col.grayMediumLight};
font-size: ${({ theme }) => theme.font.md};
padding: ${({ large }) =>
large ? "10px 30px 10px 14px" : "8px 30px 8px 10px"};
font-size: ${({ theme, large }) => (large ? theme.font.lg : theme.font.sm)};
border-radius: ${({ theme }) => theme.borderRadius};
`;
const numberFormat = tv({
base: [
"outline-0",
"min-w-[170px]",
"border border-gray-400",
"rounded",
"py-2 pr-[30px] pl-[10px]",
"text-sm",
],
variants: {
large: { true: "py-[10px] pr-[30px] pl-[14px] text-xl" },
},
});

const CurrencyInput = ({
currencyConfig,
Expand Down Expand Up @@ -46,25 +52,27 @@ const CurrencyInput = ({
setNumberFormatValue("");
}
}, [value]);
return (
<SxNumberFormat
{...currencyConfig}
suffix={` ${currencyConfig?.unit}`}
placeholder={placeholder}
type="text"
value={numberFormatValue}
large={large}
onValueChange={(values) => {
if (exists(values.floatValue) && !Number.isNaN(values.floatValue)) {
setNumberFormatValue(values.floatValue);
onChange(parseInt((values.floatValue * factor).toFixed(0), 10));
} else {
setNumberFormatValue("");
onChange(null);
}
}}
/>
);
// typed up front with an explicit base type: the polymorphic NumericFormat
// props are otherwise too complex a union for tsc
const props: NumericFormatProps = {
...currencyConfig,
className: numberFormat({ large }),
suffix: ` ${currencyConfig?.unit}`,
placeholder,
type: "text",
value: numberFormatValue,
onValueChange: (values) => {
if (exists(values.floatValue) && !Number.isNaN(values.floatValue)) {
setNumberFormatValue(values.floatValue);
onChange(parseInt((values.floatValue * factor).toFixed(0), 10));
} else {
setNumberFormatValue("");
onChange(null);
}
},
};

return <NumericFormat<InputAttributes> {...props} />;
};

export default CurrencyInput;
85 changes: 38 additions & 47 deletions frontend/src/js/ui-components/Dropzone.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import styled from "@emotion/styled";
import type { ReactNode, Ref } from "react";
import { type DropTargetMonitor, useDrop } from "react-dnd";
import { tv } from "tailwind-variants";

import { DNDType } from "../common/constants/dndTypes";
import { exists } from "../common/helpers/exists";
Expand All @@ -12,43 +12,30 @@ import type {

import type { DragItemFile } from "./DropzoneWithFileInput";

const Root = styled("div")<{
isOver?: boolean;
naked?: boolean;
bare?: boolean;
transparent?: boolean;
canDrop?: boolean;
invisible?: boolean;
}>`
border: ${({ theme, isOver, canDrop, naked }) =>
naked
? "none"
: isOver && !canDrop
? `3px solid ${theme.col.red}`
: isOver
? `3px solid ${theme.col.black}`
: `3px dashed ${theme.col.grayMediumLight}`};
border-radius: ${({ theme }) => theme.borderRadius};
padding: ${({ bare }) => (bare ? "0" : "10px")};
display: ${({ invisible }) => (invisible ? "none" : "flex")};
align-items: center;
justify-content: center;
background-color: ${({ theme, canDrop, naked, isOver, transparent }) =>
naked && isOver && canDrop
? theme.col.grayLight
: canDrop
? theme.col.grayVeryLight
: transparent
? "transparent"
: theme.col.bg};
width: 100%;
color: ${({ theme, isOver, canDrop }) =>
isOver && !canDrop
? theme.col.red
: isOver
? theme.col.black
: theme.col.gray};
`;
const root = tv({
base: [
"flex items-center justify-center",
"w-full",
"p-[10px]",
"rounded",
"border-[3px] border-dashed border-gray-400",
"bg-bg-50",
"text-gray-500",
],
variants: {
bare: { true: "p-0" },
invisible: { true: "hidden" },
// later wins when several are set
transparent: { true: "bg-transparent" },
canDrop: { true: "bg-gray-50" },
isOver: { true: "border-solid border-gray-800 text-gray-800" },
naked: { true: "border-none" },
},
compoundVariants: [
{ isOver: true, canDrop: false, class: "border-red text-red" },
{ naked: true, isOver: true, canDrop: true, class: "bg-gray-100" },
],
});

export interface ChildArgs<DroppableObject> {
isOver: boolean;
Expand Down Expand Up @@ -129,7 +116,9 @@ const Dropzone = <DroppableObject extends PossibleDroppableObject>({
});

return (
<Root
// biome-ignore lint/a11y/noStaticElementInteractions: drop target, click opens a file dialog
// biome-ignore lint/a11y/useKeyWithClickEvents: drop target, click opens a file dialog
<div
ref={(instance) => {
dropRef(instance);

Expand All @@ -142,21 +131,23 @@ const Dropzone = <DroppableObject extends PossibleDroppableObject>({
}
}
}}
isOver={isOver}
invisible={invisible && !canDropResult}
canDrop={canDropResult}
className={className}
className={root({
isOver,
invisible: !!invisible && !canDropResult,
canDrop: canDropResult,
naked,
transparent,
bare,
className,
})}
onClick={onClick}
naked={naked}
transparent={transparent}
bare={bare}
>
{children?.({
isOver,
canDrop: canDropResult,
item: item as DroppableObject, // Casting because see comment above
})}
</Root>
</div>
);
};

Expand Down
Loading
Loading