mirror of
https://github.com/swissmakers/swiss-datashare.git
synced 2026-04-09 09:37:01 +02:00
feat: improve UI for timespan inputs on admin page (#726)
* Define Timestamp type * Implement Timestamp utils * Implement Timespan input * Use timestamp input on config page * Add timespan type to config services * Refactor maxExpiration to use timespan type across services and components * Update sessionDuration to use timespan type in config and adjust token expiration logic * Update localized strings
This commit is contained in:
@@ -8,6 +8,8 @@ import {
|
||||
} from "@mantine/core";
|
||||
import { useForm } from "@mantine/form";
|
||||
import { AdminConfig, UpdateConfig } from "../../../types/config.type";
|
||||
import TimespanInput from "../../core/TimespanInput";
|
||||
import { stringToTimespan, timespanToString } from "../../../utils/date.util";
|
||||
import FileSizeInput from "../../core/FileSizeInput";
|
||||
|
||||
const AdminConfigInput = ({
|
||||
@@ -91,6 +93,13 @@ const AdminConfigInput = ({
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{configVariable.type == "timespan" && (
|
||||
<TimespanInput
|
||||
value={stringToTimespan(configVariable.value)}
|
||||
onChange={(timespan) => onValueChange(configVariable, timespanToString(timespan))}
|
||||
w={201}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
83
frontend/src/components/core/TimespanInput.tsx
Normal file
83
frontend/src/components/core/TimespanInput.tsx
Normal file
@@ -0,0 +1,83 @@
|
||||
import { useState } from "react";
|
||||
import { Timespan } from "../../types/timespan.type";
|
||||
import { NativeSelect, NumberInput } from "@mantine/core";
|
||||
import useTranslate from "../../hooks/useTranslate.hook";
|
||||
|
||||
const TimespanInput = ({ label, value, onChange, ...restProps }: {
|
||||
label?: string,
|
||||
value: Timespan,
|
||||
onChange: (timespan: Timespan) => void,
|
||||
[key: string]: any,
|
||||
}) => {
|
||||
const [unit, setUnit] = useState(value.unit);
|
||||
const [inputValue, setInputValue] = useState(value.value);
|
||||
const t = useTranslate();
|
||||
|
||||
const version = inputValue == 1 ? "singular" : "plural";
|
||||
const unitSelect = (
|
||||
<NativeSelect
|
||||
data={[
|
||||
{
|
||||
value: "minutes",
|
||||
label: t(`upload.modal.expires.minute-${version}`),
|
||||
},
|
||||
{
|
||||
value: "hours",
|
||||
label: t(`upload.modal.expires.hour-${version}`),
|
||||
},
|
||||
{
|
||||
value: "days",
|
||||
label: t(`upload.modal.expires.day-${version}`),
|
||||
},
|
||||
{
|
||||
value: "weeks",
|
||||
label: t(`upload.modal.expires.week-${version}`),
|
||||
},
|
||||
{
|
||||
value: "months",
|
||||
label: t(`upload.modal.expires.month-${version}`),
|
||||
},
|
||||
{
|
||||
value: "years",
|
||||
label: t(`upload.modal.expires.year-${version}`),
|
||||
},
|
||||
]}
|
||||
value={unit}
|
||||
rightSectionWidth={28}
|
||||
styles={{
|
||||
input: {
|
||||
fontWeight: 500,
|
||||
borderTopLeftRadius: 0,
|
||||
borderBottomLeftRadius: 0,
|
||||
width: 120,
|
||||
marginRight: -2,
|
||||
},
|
||||
}}
|
||||
onChange={event => {
|
||||
const unit = event.currentTarget.value as Timespan["unit"];
|
||||
setUnit(unit);
|
||||
onChange({ value: inputValue, unit });
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<NumberInput
|
||||
label={label}
|
||||
value={inputValue}
|
||||
min={0}
|
||||
max={999999}
|
||||
precision={0}
|
||||
rightSection={unitSelect}
|
||||
rightSectionWidth={120}
|
||||
onChange={value => {
|
||||
const inputVal = value || 0;
|
||||
setInputValue(inputVal);
|
||||
onChange({ value: inputVal, unit });
|
||||
}}
|
||||
{...restProps}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default TimespanInput;
|
||||
@@ -31,6 +31,7 @@ import { FileUpload } from "../../../types/File.type";
|
||||
import { CreateShare } from "../../../types/share.type";
|
||||
import { getExpirationPreview } from "../../../utils/date.util";
|
||||
import toast from "../../../utils/toast.util";
|
||||
import { Timespan } from "../../../types/timespan.type";
|
||||
|
||||
const showCreateUploadModal = (
|
||||
modals: ModalsContextProps,
|
||||
@@ -39,7 +40,7 @@ const showCreateUploadModal = (
|
||||
isReverseShare: boolean;
|
||||
allowUnauthenticatedShares: boolean;
|
||||
enableEmailRecepients: boolean;
|
||||
maxExpirationInHours: number;
|
||||
maxExpiration: Timespan;
|
||||
shareIdLength: number;
|
||||
simplified: boolean;
|
||||
},
|
||||
@@ -112,7 +113,7 @@ const CreateUploadModalBody = ({
|
||||
isReverseShare: boolean;
|
||||
allowUnauthenticatedShares: boolean;
|
||||
enableEmailRecepients: boolean;
|
||||
maxExpirationInHours: number;
|
||||
maxExpiration: Timespan;
|
||||
shareIdLength: number;
|
||||
};
|
||||
}) => {
|
||||
@@ -180,17 +181,17 @@ const CreateUploadModalBody = ({
|
||||
);
|
||||
|
||||
if (
|
||||
options.maxExpirationInHours != 0 &&
|
||||
options.maxExpiration.value != 0 &&
|
||||
(form.values.never_expires ||
|
||||
expirationDate.isAfter(
|
||||
moment().add(options.maxExpirationInHours, "hours"),
|
||||
moment().add(options.maxExpiration.value, options.maxExpiration.unit),
|
||||
))
|
||||
) {
|
||||
form.setFieldError(
|
||||
"expiration_num",
|
||||
t("upload.modal.expires.error.too-long", {
|
||||
max: moment
|
||||
.duration(options.maxExpirationInHours, "hours")
|
||||
.duration(options.maxExpiration.value, options.maxExpiration.unit)
|
||||
.humanize(),
|
||||
}),
|
||||
);
|
||||
@@ -327,7 +328,7 @@ const CreateUploadModalBody = ({
|
||||
/>
|
||||
</Col>
|
||||
</Grid>
|
||||
{options.maxExpirationInHours == 0 && (
|
||||
{options.maxExpiration.value == 0 && (
|
||||
<Checkbox
|
||||
label={t("upload.modal.expires.never-long")}
|
||||
{...form.getInputProps("never_expires")}
|
||||
@@ -478,7 +479,7 @@ const SimplifiedCreateUploadModalModal = ({
|
||||
isReverseShare: boolean;
|
||||
allowUnauthenticatedShares: boolean;
|
||||
enableEmailRecepients: boolean;
|
||||
maxExpirationInHours: number;
|
||||
maxExpiration: Timespan;
|
||||
shareIdLength: number;
|
||||
};
|
||||
}) => {
|
||||
|
||||
@@ -344,7 +344,7 @@ export default {
|
||||
"admin.config.share.allow-unauthenticated-shares": "Nicht authentifizierte Freigaben erlauben",
|
||||
"admin.config.share.allow-unauthenticated-shares.description": "Gibt an, ob nicht authentifizierte Benutzer Freigaben erstellen können",
|
||||
"admin.config.share.max-expiration": "Max. Ablaufdatum",
|
||||
"admin.config.share.max-expiration.description": "Maximale Ablaufzeit in Stunden. Auf 0 setzen, um kein Ablaufdatum zu definieren.",
|
||||
"admin.config.share.max-expiration.description": "Maximale Ablaufzeit. Auf 0 setzen, um kein Ablaufdatum zu definieren.",
|
||||
"admin.config.share.share-id-length": "Default share ID length",
|
||||
"admin.config.share.share-id-length.description": "Default length for the generated ID of a share. This value is also used to generate links for reverse shares. A value below 8 is not considered secure.",
|
||||
"admin.config.share.max-size": "Maximale Größe",
|
||||
|
||||
@@ -478,7 +478,7 @@ export default {
|
||||
"Whether unauthenticated users can create shares",
|
||||
"admin.config.share.max-expiration": "Max expiration",
|
||||
"admin.config.share.max-expiration.description":
|
||||
"Maximum share expiration in hours. Set to 0 to allow unlimited expiration.",
|
||||
"Maximum share expiration. Set to 0 to allow unlimited expiration.",
|
||||
"admin.config.share.share-id-length": "Default share ID length",
|
||||
"admin.config.share.share-id-length.description":
|
||||
"Default length for the generated ID of a share. This value is also used to generate links for reverse shares. A value below 8 is not considered secure.",
|
||||
|
||||
@@ -139,7 +139,7 @@ const Upload = ({
|
||||
"share.allowUnauthenticatedShares",
|
||||
),
|
||||
enableEmailRecepients: config.get("email.enableShareEmailRecipients"),
|
||||
maxExpirationInHours: config.get("share.maxExpiration"),
|
||||
maxExpiration: config.get("share.maxExpiration"),
|
||||
shareIdLength: config.get("share.shareIdLength"),
|
||||
simplified,
|
||||
},
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import axios from "axios";
|
||||
import Config, { AdminConfig, UpdateConfig } from "../types/config.type";
|
||||
import api from "./api.service";
|
||||
import { stringToTimespan } from "../utils/date.util";
|
||||
|
||||
const list = async (): Promise<Config[]> => {
|
||||
return (await api.get("/configs")).data;
|
||||
@@ -30,6 +31,8 @@ const get = (key: string, configVariables: Config[]): any => {
|
||||
if (configVariable.type == "boolean") return value == "true";
|
||||
if (configVariable.type == "string" || configVariable.type == "text")
|
||||
return value;
|
||||
if (configVariable.type == "timespan")
|
||||
return stringToTimespan(value);
|
||||
};
|
||||
|
||||
const finishSetup = async (): Promise<AdminConfig[]> => {
|
||||
|
||||
2
frontend/src/types/timespan.type.ts
Normal file
2
frontend/src/types/timespan.type.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export type TimeUnit = "minutes" | "hours" | "days" | "weeks" | "months" | "years";
|
||||
export type Timespan = { value: number; unit: TimeUnit };
|
||||
@@ -1,4 +1,5 @@
|
||||
import moment from "moment";
|
||||
import { Timespan } from "../types/timespan.type";
|
||||
|
||||
export const getExpirationPreview = (
|
||||
messages: {
|
||||
@@ -30,3 +31,14 @@ export const getExpirationPreview = (
|
||||
moment(expirationDate).format("LLL"),
|
||||
);
|
||||
};
|
||||
|
||||
export const timespanToString = (timespan: Timespan) => {
|
||||
return `${timespan.value} ${timespan.unit}`;
|
||||
}
|
||||
|
||||
export const stringToTimespan = (value: string): Timespan => {
|
||||
return {
|
||||
value: parseInt(value.split(" ")[0]),
|
||||
unit: value.split(" ")[1],
|
||||
} as Timespan;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user