fix(dashboard,ui): ConditionBlock styling (#10481)

**What**
- Resolves CMRC-58
- Also fixes some other issues, TS errors, Eslint warnings etc. in the Promotion domain. There are still several TS errors as the return types from `@medusajs/types` don't seem to match how they are used here, but I have left that as is, as I am not super familiar with the Promotion module.
This commit is contained in:
Kasper Fabricius Kristensen
2024-12-09 18:42:19 +01:00
committed by GitHub
parent 3409953c4f
commit c9a66b19af
8 changed files with 212 additions and 133 deletions

View File

@@ -0,0 +1,6 @@
---
"@medusajs/ui": patch
"@medusajs/dashboard": patch
---
fix(dashboard,ui): Bring ConditionBlock in line with design

View File

@@ -9,6 +9,7 @@ import {
Separator as PrimitiveSeparator,
} from "@ariakit/react"
import {
CheckMini,
EllipseMiniSolid,
PlusMini,
TrianglesMini,
@@ -290,7 +291,7 @@ const ComboboxImpl = <T extends Value = string>(
ref={comboboxRef}
onFocus={() => setOpen(true)}
className={clx(
"txt-compact-small text-ui-fg-base placeholder:text-ui-fg-subtle transition-fg size-full cursor-pointer bg-transparent pl-2 pr-8 outline-none focus:cursor-text",
"txt-compact-small text-ui-fg-base !placeholder:text-ui-fg-muted transition-fg size-full cursor-pointer bg-transparent pl-2 pr-8 outline-none focus:cursor-text",
"hover:bg-ui-bg-field-hover",
{
"opacity-0": hideInput,
@@ -349,7 +350,7 @@ const ComboboxImpl = <T extends Value = string>(
)}
>
<PrimitiveComboboxItemCheck className="flex !size-5 items-center justify-center">
<EllipseMiniSolid />
{isArrayValue ? <CheckMini /> : <EllipseMiniSolid />}
</PrimitiveComboboxItemCheck>
<PrimitiveComboboxItemValue className="txt-compact-small">
{label}

View File

@@ -1,6 +1,6 @@
import { RuleAttributeOptionsResponse, StoreDTO } from "@medusajs/types"
import { Input, Select } from "@medusajs/ui"
import { RefCallBack, useWatch } from "react-hook-form"
import { useWatch } from "react-hook-form"
import { Form } from "../../../../../../components/common/form"
import { Combobox } from "../../../../../../components/inputs/combobox"
import { usePromotionRuleValues } from "../../../../../../hooks/api/promotions"
@@ -13,9 +13,8 @@ type RuleValueFormFieldType = {
| "application_method.buy_rules"
| "rules"
| "application_method.target_rules"
valuesField: any
operatorsField: any
valuesRef: RefCallBack
name: string
operator: string
fieldRule: any
attributes: RuleAttributeOptionsResponse[]
ruleType: "rules" | "target-rules" | "buy-rules"
@@ -39,9 +38,8 @@ export const RuleValueFormField = ({
form,
identifier,
scope,
valuesField,
operatorsField,
valuesRef,
name,
operator,
fieldRule,
attributes,
ruleType,
@@ -65,13 +63,13 @@ export const RuleValueFormField = ({
const watchOperator = useWatch({
control: form.control,
name: operatorsField.name,
name: operator,
})
return (
<Form.Field
key={`${identifier}.${scope}.${valuesField.name}-${fieldRule.attribute}`}
{...valuesField}
key={`${identifier}.${scope}.${name}-${fieldRule.attribute}`}
name={name}
render={({ field: { onChange, ref, ...field } }) => {
if (attribute?.field_type === "number") {
return (
@@ -82,7 +80,7 @@ export const RuleValueFormField = ({
type="number"
onChange={onChange}
className="bg-ui-bg-base"
ref={valuesRef}
ref={ref}
min={1}
disabled={!fieldRule.attribute}
/>
@@ -96,6 +94,7 @@ export const RuleValueFormField = ({
<Form.Control>
<Input
{...field}
ref={ref}
onChange={onChange}
className="bg-ui-bg-base"
disabled={!fieldRule.attribute}
@@ -143,6 +142,7 @@ export const RuleValueFormField = ({
<Form.Control>
<Combobox
{...field}
ref={ref}
placeholder="Select Values"
options={options}
onChange={onChange}

View File

@@ -1,8 +1,13 @@
import { XMarkMini } from "@medusajs/icons"
import { PromotionDTO } from "@medusajs/types"
import { Badge, Button, Heading, Select, Text } from "@medusajs/ui"
import { Fragment, useEffect } from "react"
import { useFieldArray, UseFormReturn, useWatch } from "react-hook-form"
import { Badge, Button, Heading, IconButton, Select, Text } from "@medusajs/ui"
import { forwardRef, Fragment, useEffect } from "react"
import {
ControllerRenderProps,
useFieldArray,
UseFormReturn,
useWatch,
} from "react-hook-form"
import { useTranslation } from "react-i18next"
import { Form } from "../../../../../../components/common/form"
import {
@@ -102,7 +107,16 @@ export const RulesFormField = ({
replace(generateRuleAttributes(rulesToAppend) as any)
}
}, [promotionType, isLoading])
}, [
promotionType,
isLoading,
ruleType,
fields.length,
form,
replace,
rules,
promotion?.id,
])
return (
<div className="flex flex-col">
@@ -116,24 +130,16 @@ export const RulesFormField = ({
{fields.map((fieldRule: any, index) => {
const identifier = fieldRule.id
const { ref: attributeRef, ...attributeField } = form.register(
`${scope}.${index}.attribute`
)
const { ref: operatorRef, ...operatorsField } = form.register(
`${scope}.${index}.operator`
)
const { ref: valuesRef, ...valuesField } = form.register(
`${scope}.${index}.values`
)
return (
<Fragment key={`${fieldRule.id}.${index}.${fieldRule.attribute}`}>
<div className="bg-ui-bg-subtle border-ui-border-base flex flex-row gap-2 rounded-xl border px-2 py-2">
<div className="grow">
<Form.Field
key={`${identifier}.${scope}.${attributeField.name}`}
{...attributeField}
render={({ field: { onChange, ref, ...field } }) => {
name={`${scope}.${index}.attribute`}
render={({ field }) => {
const { onChange, ref, ...fieldProps } = field
const existingAttributes =
fields?.map((field: any) => field.attribute) || []
const attributeOptions =
@@ -145,55 +151,71 @@ export const RulesFormField = ({
return !existingAttributes.includes(attr.value)
}) || []
const disabled = !!fieldRule.required
const onValueChange = (e: string) => {
const currentAttributeOption = attributeOptions.find(
(ao) => ao.id === e
)
update(index, {
...fieldRule,
values: [],
disguised: currentAttributeOption?.disguised || false,
})
onChange(e)
}
return (
<Form.Item className="mb-2">
{fieldRule.required && (
<p className="text text-ui-fg-muted txt-small">
{t("promotions.form.required")}
</p>
<div className="flex items-center px-2">
<p className="text text-ui-fg-muted txt-small">
{t("promotions.form.required")}
</p>
</div>
)}
<Form.Control>
<Select
{...field}
onValueChange={(e) => {
const currentAttributeOption =
attributeOptions.find((ao) => ao.id === e)
update(index, {
...fieldRule,
values: [],
disguised:
currentAttributeOption?.disguised || false,
})
onChange(e)
}}
disabled={fieldRule.required}
>
<Select.Trigger
ref={attributeRef}
className="bg-ui-bg-base"
{!disabled ? (
<Select
{...fieldProps}
onValueChange={onValueChange}
disabled={fieldRule.required}
>
<Select.Value
placeholder={t(
"promotions.form.selectAttribute"
)}
/>
</Select.Trigger>
<Select.Trigger
ref={ref}
className="bg-ui-bg-base"
>
<Select.Value
placeholder={t(
"promotions.form.selectAttribute"
)}
/>
</Select.Trigger>
<Select.Content>
{attributeOptions?.map((c, i) => (
<Select.Item
key={`${identifier}-attribute-option-${i}`}
value={c.value}
>
<span className="text-ui-fg-subtle">
{c.label}
</span>
</Select.Item>
))}
</Select.Content>
</Select>
<Select.Content>
{attributeOptions?.map((c, i) => (
<Select.Item
key={`${identifier}-attribute-option-${i}`}
value={c.value}
>
<span className="text-ui-fg-subtle">
{c.label}
</span>
</Select.Item>
))}
</Select.Content>
</Select>
) : (
<DisabledField
label={
attributeOptions?.find(
(ao) => ao.value === fieldRule.attribute
)?.label || ""
}
field={field}
/>
)}
</Form.Control>
<Form.ErrorMessage />
</Form.Item>
@@ -203,43 +225,60 @@ export const RulesFormField = ({
<div className="flex gap-2">
<Form.Field
key={`${identifier}.${scope}.${operatorsField.name}`}
{...operatorsField}
render={({ field: { onChange, ref, ...field } }) => {
const currentAttributeOption = attributes.find(
name={`${scope}.${index}.operator`}
render={({ field }) => {
const { onChange, ref, ...fieldProps } = field
const currentAttributeOption = attributes?.find(
(attr) => attr.value === fieldRule.attribute
)
const options =
currentAttributeOption?.operators?.map((o, idx) => ({
label: o.label,
value: o.value,
key: `${identifier}-operator-option-${idx}`,
})) || []
const disabled =
!!fieldRule.attribute && options?.length <= 1
return (
<Form.Item className="basis-1/2">
<Form.Control>
<Select
{...field}
disabled={!fieldRule.attribute}
onValueChange={onChange}
>
<Select.Trigger
ref={operatorRef}
className="bg-ui-bg-base"
{!disabled ? (
<Select
{...fieldProps}
disabled={!fieldRule.attribute}
onValueChange={onChange}
>
<Select.Value placeholder="Select Operator" />
</Select.Trigger>
<Select.Trigger
ref={ref}
className="bg-ui-bg-base"
>
<Select.Value placeholder="Select Operator" />
</Select.Trigger>
<Select.Content>
{currentAttributeOption?.operators?.map(
(c, i) => (
<Select.Item
key={`${identifier}-operator-option-${i}`}
value={c.value}
>
<Select.Content>
{options?.map((c) => (
<Select.Item key={c.key} value={c.value}>
<span className="text-ui-fg-subtle">
{c.label}
</span>
</Select.Item>
)
)}
</Select.Content>
</Select>
))}
</Select.Content>
</Select>
) : (
<DisabledField
label={
options.find(
(o) => o.value === fieldProps.value
)?.label || ""
}
field={field}
/>
)}
</Form.Control>
<Form.ErrorMessage />
</Form.Item>
@@ -251,9 +290,8 @@ export const RulesFormField = ({
form={form}
identifier={identifier}
scope={scope}
valuesField={valuesField}
operatorsField={operatorsField}
valuesRef={valuesRef}
name={`${scope}.${index}.values`}
operator={`${scope}.${index}.operator`}
fieldRule={fieldRule}
attributes={attributes}
ruleType={ruleType}
@@ -261,20 +299,25 @@ export const RulesFormField = ({
</div>
</div>
<div className="flex-none self-center px-1">
<XMarkMini
className={`text-ui-fg-muted cursor-pointer ${
fieldRule.required ? "invisible" : "visible"
}`}
onClick={() => {
if (!fieldRule.required) {
setRulesToRemove &&
setRulesToRemove([...rulesToRemove, fieldRule])
<div className="size-7 flex-none self-center">
{!fieldRule.required && (
<IconButton
size="small"
variant="transparent"
className="text-ui-fg-muted"
type="button"
onClick={() => {
if (!fieldRule.required) {
setRulesToRemove &&
setRulesToRemove([...rulesToRemove, fieldRule])
remove(index)
}
}}
/>
remove(index)
}
}}
>
<XMarkMini />
</IconButton>
)}
</div>
</div>
@@ -291,7 +334,7 @@ export const RulesFormField = ({
)
})}
<div className={!!fields.length ? "mt-6" : ""}>
<div className={fields.length ? "mt-6" : ""}>
<Button
type="button"
variant="secondary"
@@ -330,3 +373,27 @@ export const RulesFormField = ({
</div>
)
}
type DisabledAttributeProps = {
label: string
field: ControllerRenderProps
}
/**
* Render this if an attribute is disabled, or
* if there is only one option available.
*/
const DisabledField = forwardRef<HTMLInputElement, DisabledAttributeProps>(
({ label, field }, ref) => {
return (
<div>
<div className="txt-compact-small bg-ui-bg-component shadow-borders-base text-ui-fg-base h-8 rounded-md px-2 py-1.5">
{label}
</div>
<input {...field} ref={ref} disabled hidden />
</div>
)
}
)
DisabledField.displayName = "DisabledField"

View File

@@ -79,6 +79,7 @@ export const CreatePromotionForm = () => {
defaultValues,
resolver: zodResolver(CreatePromotionSchema),
})
const { setValue, reset, getValues } = form
const { mutateAsync: createPromotion } = useCreatePromotion()
@@ -150,7 +151,7 @@ export const CreatePromotionForm = () => {
})
)
handleSuccess()
handleSuccess(`/promotions/${promotion.id}`)
},
onError: (e) => {
toast.error(e.message)
@@ -244,20 +245,20 @@ export const CreatePromotionForm = () => {
return
}
form.reset({ ...defaultValues, template_id: watchTemplateId })
reset({ ...defaultValues, template_id: watchTemplateId })
for (const [key, value] of Object.entries(currentTemplate.defaults)) {
if (typeof value === "object") {
for (const [subKey, subValue] of Object.entries(value)) {
form.setValue(`application_method.${subKey}`, subValue)
setValue(`application_method.${subKey}`, subValue)
}
} else {
form.setValue(key, value)
setValue(key, value)
}
}
return currentTemplate
}, [watchTemplateId])
}, [watchTemplateId, setValue, reset])
const watchValueType = useWatch({
control: form.control,
@@ -272,9 +273,9 @@ export const CreatePromotionForm = () => {
useEffect(() => {
if (watchAllocation === "across") {
form.setValue("application_method.max_quantity", null)
setValue("application_method.max_quantity", null)
}
}, [watchAllocation])
}, [watchAllocation, setValue])
const watchType = useWatch({
control: form.control,
@@ -307,19 +308,19 @@ export const CreatePromotionForm = () => {
})
useEffect(() => {
const formData = form.getValues()
const formData = getValues()
if (watchCampaignChoice !== "existing") {
form.setValue("campaign_id", undefined)
setValue("campaign_id", undefined)
}
if (watchCampaignChoice !== "new") {
form.setValue("campaign", undefined)
setValue("campaign", undefined)
}
if (watchCampaignChoice === "new") {
if (!formData.campaign || !formData.campaign?.budget?.type) {
form.setValue("campaign", {
setValue("campaign", {
...DEFAULT_CAMPAIGN_VALUES,
budget: {
...DEFAULT_CAMPAIGN_VALUES.budget,
@@ -328,7 +329,7 @@ export const CreatePromotionForm = () => {
})
}
}
}, [watchCampaignChoice])
}, [watchCampaignChoice, getValues, setValue])
const watchRules = useWatch({
control: form.control,

View File

@@ -85,7 +85,7 @@ export const PromotionGeneralSection = ({
[PromotionStatus.EXPIRED]: ["red", t("statuses.expired")],
}[getPromotionStatus(promotion)] as [
"grey" | "orange" | "green" | "red",
string,
string
]
const displayValue = getDisplayValue(promotion)
@@ -141,7 +141,11 @@ export const PromotionGeneralSection = ({
{t("fields.code")}
</Text>
<Copy content={promotion.code!} asChild>
<Copy
content={promotion.code!}
className="text-ui-tag-neutral-text"
asChild
>
<Badge
size="2xsmall"
rounded="full"

View File

@@ -90,7 +90,7 @@ const Copy = React.forwardRef<HTMLButtonElement, CopyProps>(
aria-label="Copy code snippet"
type="button"
className={clx(
"text-ui-contrast-fg-secondary h-fit w-fit",
"h-fit w-fit",
className
)}
onClick={copyToClipboard}
@@ -100,14 +100,14 @@ const Copy = React.forwardRef<HTMLButtonElement, CopyProps>(
children
) : done ? (
isDefault ? (
<CheckCircleSolid />
<CheckCircleSolid className="text-ui-fg-subtle" />
) : (
<CheckCircleMiniSolid />
<CheckCircleMiniSolid className="text-ui-fg-subtle" />
)
) : isDefault ? (
<SquareTwoStack />
<SquareTwoStack className="text-ui-fg-subtle" />
) : (
<SquareTwoStackMini />
<SquareTwoStackMini className="text-ui-fg-subtle" />
)}
</Component>
</Tooltip>

View File

@@ -1,6 +1,6 @@
"use client"
import { EllipseMiniSolid, TrianglesMini } from "@medusajs/icons"
import { Check, TrianglesMini } from "@medusajs/icons"
import * as SelectPrimitive from "@radix-ui/react-select"
import { cva } from "cva"
import * as React from "react"
@@ -194,7 +194,7 @@ const Item = React.forwardRef<
>
<span className="flex h-[15px] w-[15px] items-center justify-center">
<SelectPrimitive.ItemIndicator className="flex items-center justify-center">
<EllipseMiniSolid />
<Check />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText className="flex-1 truncate">