improvement: allow free number entry for max views in secret sharing

This commit is contained in:
Scott Wilson
2025-06-03 12:50:22 -07:00
parent 274952544f
commit 8f97b3ad87
2 changed files with 120 additions and 60 deletions
@@ -71,7 +71,8 @@ const formSchema = z
maxLifetimeUnit: z.enum(["m", "h", "d"], { maxLifetimeUnit: z.enum(["m", "h", "d"], {
invalid_type_error: "Please select a valid time unit" invalid_type_error: "Please select a valid time unit"
}), }),
maxViewLimit: z.string() maxViewLimit: z.string(),
shouldLimitView: z.boolean()
}) })
.superRefine((data, ctx) => { .superRefine((data, ctx) => {
const { maxLifetimeValue, maxLifetimeUnit } = data; const { maxLifetimeValue, maxLifetimeUnit } = data;
@@ -112,8 +113,8 @@ const formSchema = z
type TForm = z.infer<typeof formSchema>; type TForm = z.infer<typeof formSchema>;
const viewLimitOptions = [ const viewLimitOptions = [
{ label: "1", value: 1 }, { label: "Unlimited", value: false },
{ label: "Unlimited", value: -1 } { label: "Limited", value: true }
]; ];
export const OrgSecretShareLimitSection = () => { export const OrgSecretShareLimitSection = () => {
@@ -125,13 +126,15 @@ export const OrgSecretShareLimitSection = () => {
return { return {
maxLifetimeValue: initialLifetime.maxLifetimeValue, maxLifetimeValue: initialLifetime.maxLifetimeValue,
maxLifetimeUnit: initialLifetime.maxLifetimeUnit, maxLifetimeUnit: initialLifetime.maxLifetimeUnit,
maxViewLimit: currentOrg?.maxSharedSecretViewLimit?.toString() || "-1" maxViewLimit: currentOrg?.maxSharedSecretViewLimit?.toString() || "1",
shouldLimitView: Boolean(currentOrg?.maxSharedSecretViewLimit)
}; };
}; };
const { const {
control, control,
formState: { isSubmitting, isDirty }, formState: { isSubmitting, isDirty },
watch,
handleSubmit, handleSubmit,
reset reset
} = useForm<TForm>({ } = useForm<TForm>({
@@ -139,6 +142,8 @@ export const OrgSecretShareLimitSection = () => {
defaultValues: getDefaultFormValues() defaultValues: getDefaultFormValues()
}); });
const shouldLimitView = watch("shouldLimitView");
useEffect(() => { useEffect(() => {
if (currentOrg) { if (currentOrg) {
reset(getDefaultFormValues()); reset(getDefaultFormValues());
@@ -154,8 +159,7 @@ export const OrgSecretShareLimitSection = () => {
await mutateAsync({ await mutateAsync({
orgId: currentOrg.id, orgId: currentOrg.id,
maxSharedSecretViewLimit: maxSharedSecretViewLimit: formData.shouldLimitView ? Number(formData.maxViewLimit) : null,
formData.maxViewLimit === "-1" ? null : Number(formData.maxViewLimit),
maxSharedSecretLifetime: maxSharedSecretLifetimeSeconds maxSharedSecretLifetime: maxSharedSecretLifetimeSeconds
}); });
@@ -249,26 +253,32 @@ export const OrgSecretShareLimitSection = () => {
)} )}
/> />
</div> </div>
<div className="flex max-w-sm"> <div className="flex max-w-sm items-end gap-2">
<Controller <Controller
control={control} control={control}
name="maxViewLimit" name="shouldLimitView"
render={({ field: { onChange, ...field }, fieldState: { error } }) => ( render={({ field: { onChange, value, ...field }, fieldState: { error } }) => (
<FormControl <FormControl
label="Max Views" label="Max Views"
errorText={error?.message} errorText={error?.message}
isError={Boolean(error)} isError={Boolean(error)}
className="w-full" className="w-48"
> >
<Select <Select
defaultValue={field.value} defaultValue={value.toString()}
{...field} value={value.toString()}
onValueChange={(e) => onChange(e)} onValueChange={(e) => onChange(e === "true")}
className="w-full" className="w-full"
position="popper"
dropdownContainerClassName="max-w-none"
isDisabled={!isAllowed} isDisabled={!isAllowed}
{...field}
> >
{viewLimitOptions.map(({ label, value: viewLimitValue }) => ( {viewLimitOptions.map(({ label, value: viewLimitValue }) => (
<SelectItem value={String(viewLimitValue || "")} key={label}> <SelectItem
value={viewLimitValue.toString()}
key={viewLimitValue.toString()}
>
{label} {label}
</SelectItem> </SelectItem>
))} ))}
@@ -276,6 +286,29 @@ export const OrgSecretShareLimitSection = () => {
</FormControl> </FormControl>
)} )}
/> />
{shouldLimitView && (
<Controller
control={control}
name="maxViewLimit"
render={({ field: { onChange, value, ...field }, fieldState: { error } }) => (
<FormControl
errorText={error?.message}
isError={Boolean(error)}
className="w-48"
>
<Input
value={value}
onChange={onChange}
{...field}
min={1}
max={1000}
type="number"
isDisabled={!isAllowed}
/>
</FormControl>
)}
/>
)}
</div> </div>
<Button <Button
colorSchema="secondary" colorSchema="secondary"
@@ -35,8 +35,8 @@ const expiresInOptions = [
]; ];
const viewLimitOptions = [ const viewLimitOptions = [
{ label: "1", value: 1 }, { label: "Unlimited", value: false },
{ label: "Unlimited", value: -1 } { label: "Limited", value: true }
]; ];
const schema = z.object({ const schema = z.object({
@@ -45,6 +45,7 @@ const schema = z.object({
secret: z.string().min(1), secret: z.string().min(1),
expiresIn: z.string(), expiresIn: z.string(),
viewLimit: z.string(), viewLimit: z.string(),
shouldLimitView: z.boolean(),
accessType: z.nativeEnum(SecretSharingAccessType).optional(), accessType: z.nativeEnum(SecretSharingAccessType).optional(),
emails: z emails: z
.string() .string()
@@ -96,25 +97,25 @@ export const ShareSecretForm = ({
? expiresInOptions.filter((v) => v.value / 1000 <= maxSharedSecretLifetime) ? expiresInOptions.filter((v) => v.value / 1000 <= maxSharedSecretLifetime)
: expiresInOptions; : expiresInOptions;
const filteredViewLimitOptions = maxSharedSecretViewLimit
? viewLimitOptions.filter((v) => v.value > 0 && v.value <= maxSharedSecretViewLimit)
: viewLimitOptions;
const { const {
control, control,
reset, reset,
handleSubmit, handleSubmit,
formState: { isSubmitting } formState: { isSubmitting },
watch
} = useForm<FormData>({ } = useForm<FormData>({
resolver: zodResolver(schema), resolver: zodResolver(schema),
defaultValues: { defaultValues: {
secret: value || "", secret: value || "",
viewLimit: filteredViewLimitOptions[filteredViewLimitOptions.length - 1].value.toString(), viewLimit: maxSharedSecretViewLimit?.toString() ?? "1",
shouldLimitView: Boolean(maxSharedSecretViewLimit),
expiresIn: expiresIn:
filteredExpiresInOptions[Math.min(filteredExpiresInOptions.length - 1, 2)].value.toString() filteredExpiresInOptions[Math.min(filteredExpiresInOptions.length - 1, 2)].value.toString()
} }
}); });
const isLimitingView = watch("shouldLimitView");
const onFormSubmit = async ({ const onFormSubmit = async ({
name, name,
password, password,
@@ -122,7 +123,8 @@ export const ShareSecretForm = ({
expiresIn, expiresIn,
viewLimit, viewLimit,
accessType, accessType,
emails emails,
shouldLimitView
}: FormData) => { }: FormData) => {
try { try {
const expiresAt = new Date(new Date().getTime() + Number(expiresIn)); const expiresAt = new Date(new Date().getTime() + Number(expiresIn));
@@ -134,7 +136,7 @@ export const ShareSecretForm = ({
password, password,
secretValue: secret, secretValue: secret,
expiresAt, expiresAt,
expiresAfterViews: viewLimit === "-1" ? undefined : Number(viewLimit), expiresAfterViews: shouldLimitView ? Number(viewLimit) : undefined,
accessType, accessType,
emails: processedEmails emails: processedEmails
}); });
@@ -320,44 +322,69 @@ export const ShareSecretForm = ({
</FormControl> </FormControl>
)} )}
/> />
<Controller <div className="flex w-full items-end gap-2">
control={control} {maxSharedSecretViewLimit === null && (
name="viewLimit" <Controller
render={({ field: { onChange, ...field }, fieldState: { error } }) => ( control={control}
<FormControl name="shouldLimitView"
label="Max Views" render={({ field: { onChange, ...field }, fieldState: { error } }) => (
errorText={error?.message} <FormControl
isError={Boolean(error)} label="Max Views"
helperText={ errorText={error?.message}
viewLimitOptions.length !== filteredViewLimitOptions.length ? ( isError={Boolean(error)}
<span className="text-yellow-500"> className="flex-1"
Limited to{" "} >
{filteredViewLimitOptions[filteredViewLimitOptions.length - 1].label} by <Select
organization defaultValue={field.value.toString()}
</span> onValueChange={(e) => onChange(e === "true")}
) : undefined className="w-full"
} position="popper"
> {...field}
<Select value={field.value.toString()}
defaultValue={field.value} dropdownContainerClassName="max-w-none"
{...field}
onValueChange={(e) => onChange(e)}
className="w-full"
>
{viewLimitOptions.map(({ label, value: viewLimitValue }) => (
<SelectItem
value={String(viewLimitValue || "")}
key={label}
isDisabled={!filteredViewLimitOptions.some((v) => v.label === label)}
> >
{label} {viewLimitOptions.map(({ label, value: viewLimitValue }) => (
</SelectItem> <SelectItem
))} value={viewLimitValue.toString()}
</Select> key={viewLimitValue.toString()}
</FormControl> >
{label}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
)} )}
/> {isLimitingView && (
<Controller
control={control}
name="viewLimit"
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label={maxSharedSecretViewLimit ? "Max Views" : undefined}
errorText={error?.message}
isError={Boolean(error)}
className="flex-1"
helperText={
maxSharedSecretViewLimit
? `Limited to ${maxSharedSecretViewLimit} by organization.`
: undefined
}
>
<Input
onChange={onChange}
{...field}
min={1}
max={maxSharedSecretViewLimit ?? 1000}
type="number"
className="h-[37px]"
/>
</FormControl>
)}
/>
)}
</div>
{!isPublic && ( {!isPublic && (
<Controller <Controller
control={control} control={control}