mirror of
https://github.com/lotsoftick/openclaw_client.git
synced 2026-08-14 08:52:46 +00:00
refactored forms
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import { useFormik } from 'formik';
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
@@ -20,43 +21,54 @@ import {
|
||||
} from '../../entities/channel/api';
|
||||
import providerEmoji from './providerEmoji';
|
||||
|
||||
const inputSx = {
|
||||
mb: 1.5,
|
||||
'& .MuiOutlinedInput-root': { borderRadius: 1.5 },
|
||||
'& input': { fontSize: '0.85rem' },
|
||||
'& label': { fontSize: '0.85rem' },
|
||||
};
|
||||
|
||||
export default function AddChannelForm({ onDone }: { onDone: () => void }) {
|
||||
const [addChannel, { isLoading }] = useAddChannelMutation();
|
||||
const [provider, setProvider] = useState<ChannelProvider | ''>('');
|
||||
const [fields, setFields] = useState<Record<string, string>>({});
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const fieldDefs = provider ? (CHANNEL_FIELDS[provider] ?? []) : [];
|
||||
const formik = useFormik({
|
||||
initialValues: {
|
||||
provider: '' as ChannelProvider | '',
|
||||
fields: {} as Record<string, string>,
|
||||
},
|
||||
onSubmit: async (values) => {
|
||||
if (!values.provider) return;
|
||||
setError('');
|
||||
try {
|
||||
await addChannel({ channel: values.provider, ...values.fields }).unwrap();
|
||||
onDone();
|
||||
} catch (err: unknown) {
|
||||
const msg = (err as { data?: { error?: string } })?.data?.error;
|
||||
setError(msg || 'Failed to add channel');
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const fieldDefs = formik.values.provider
|
||||
? (CHANNEL_FIELDS[formik.values.provider] ?? [])
|
||||
: [];
|
||||
|
||||
const hasRequired = fieldDefs
|
||||
.filter((f) => f.required)
|
||||
.every((f) => formik.values.fields[f.key]?.trim());
|
||||
|
||||
const handleProviderChange = (val: string) => {
|
||||
setProvider(val as ChannelProvider);
|
||||
setFields({});
|
||||
formik.setValues({ provider: val as ChannelProvider, fields: {} });
|
||||
setError('');
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!provider) return;
|
||||
setError('');
|
||||
try {
|
||||
await addChannel({ channel: provider, ...fields }).unwrap();
|
||||
onDone();
|
||||
} catch (err: unknown) {
|
||||
const msg = (err as { data?: { error?: string } })?.data?.error;
|
||||
setError(msg || 'Failed to add channel');
|
||||
}
|
||||
const handleFieldChange = (key: string, val: string) => {
|
||||
formik.setFieldValue('fields', { ...formik.values.fields, [key]: val });
|
||||
};
|
||||
|
||||
const hasRequired = fieldDefs.filter((f) => f.required).every((f) => fields[f.key]?.trim());
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
p: 2,
|
||||
mb: 2,
|
||||
borderRadius: 2,
|
||||
bgcolor: 'action.hover',
|
||||
}}
|
||||
>
|
||||
<Box sx={{ p: 2, mb: 2, borderRadius: 2, bgcolor: 'action.hover' }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
|
||||
<Typography sx={{ fontSize: '0.85rem', fontWeight: 700, flex: 1 }}>Add Channel</Typography>
|
||||
<IconButton size="small" onClick={onDone}>
|
||||
@@ -64,54 +76,51 @@ export default function AddChannelForm({ onDone }: { onDone: () => void }) {
|
||||
</IconButton>
|
||||
</Box>
|
||||
|
||||
<FormControl fullWidth size="small" sx={{ mb: 2 }}>
|
||||
<InputLabel sx={{ fontSize: '0.85rem' }}>Channel Provider</InputLabel>
|
||||
<Select
|
||||
value={provider}
|
||||
label="Channel Provider"
|
||||
onChange={(e) => handleProviderChange(e.target.value)}
|
||||
sx={{ fontSize: '0.85rem' }}
|
||||
>
|
||||
{CHANNEL_PROVIDERS.map((p) => (
|
||||
<MenuItem key={p} value={p} sx={{ fontSize: '0.85rem' }}>
|
||||
{providerEmoji[p] || '📡'} {p}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<form onSubmit={formik.handleSubmit}>
|
||||
<FormControl fullWidth size="small" sx={{ mb: 2 }}>
|
||||
<InputLabel sx={{ fontSize: '0.85rem' }}>Channel Provider</InputLabel>
|
||||
<Select
|
||||
value={formik.values.provider}
|
||||
label="Channel Provider"
|
||||
onChange={(e) => handleProviderChange(e.target.value)}
|
||||
sx={{ fontSize: '0.85rem' }}
|
||||
>
|
||||
{CHANNEL_PROVIDERS.map((p) => (
|
||||
<MenuItem key={p} value={p} sx={{ fontSize: '0.85rem' }}>
|
||||
{providerEmoji[p] || '📡'} {p}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
{fieldDefs.map((f) => (
|
||||
<TextField
|
||||
key={f.key}
|
||||
fullWidth
|
||||
{fieldDefs.map((f) => (
|
||||
<TextField
|
||||
key={f.key}
|
||||
fullWidth
|
||||
size="small"
|
||||
label={f.label}
|
||||
type={f.secret ? 'password' : 'text'}
|
||||
required={f.required}
|
||||
value={formik.values.fields[f.key] || ''}
|
||||
onChange={(e) => handleFieldChange(f.key, e.target.value)}
|
||||
sx={inputSx}
|
||||
/>
|
||||
))}
|
||||
|
||||
{error && (
|
||||
<Typography sx={{ fontSize: '0.75rem', color: 'error.main', mb: 1 }}>{error}</Typography>
|
||||
)}
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
variant="contained"
|
||||
size="small"
|
||||
label={f.label}
|
||||
type={f.secret ? 'password' : 'text'}
|
||||
required={f.required}
|
||||
value={fields[f.key] || ''}
|
||||
onChange={(e) => setFields((prev) => ({ ...prev, [f.key]: e.target.value }))}
|
||||
sx={{
|
||||
mb: 1.5,
|
||||
'& .MuiOutlinedInput-root': { borderRadius: 1.5 },
|
||||
'& input': { fontSize: '0.85rem' },
|
||||
'& label': { fontSize: '0.85rem' },
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
|
||||
{error && (
|
||||
<Typography sx={{ fontSize: '0.75rem', color: 'error.main', mb: 1 }}>{error}</Typography>
|
||||
)}
|
||||
|
||||
<Button
|
||||
variant="contained"
|
||||
size="small"
|
||||
disabled={!provider || !hasRequired || isLoading}
|
||||
onClick={handleSubmit}
|
||||
sx={{ textTransform: 'none', fontSize: '0.8rem' }}
|
||||
>
|
||||
{isLoading ? <CircularProgress size={16} /> : 'Add Channel'}
|
||||
</Button>
|
||||
disabled={!formik.values.provider || !hasRequired || isLoading}
|
||||
sx={{ textTransform: 'none', fontSize: '0.8rem' }}
|
||||
>
|
||||
{isLoading ? <CircularProgress size={16} /> : 'Add Channel'}
|
||||
</Button>
|
||||
</form>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useFormik } from 'formik';
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
@@ -50,59 +51,79 @@ const inputSx = {
|
||||
'& textarea': { fontSize: '0.85rem' },
|
||||
};
|
||||
|
||||
interface CronFormValues {
|
||||
name: string;
|
||||
scheduleKind: ScheduleKind;
|
||||
scheduleValue: string;
|
||||
atDatetime: string;
|
||||
message: string;
|
||||
agent: string;
|
||||
session: string;
|
||||
tz: string;
|
||||
}
|
||||
|
||||
const initialValues: CronFormValues = {
|
||||
name: '',
|
||||
scheduleKind: 'cron',
|
||||
scheduleValue: '',
|
||||
atDatetime: '',
|
||||
message: '',
|
||||
agent: '',
|
||||
session: 'isolated',
|
||||
tz: '',
|
||||
};
|
||||
|
||||
export default function AddCronForm({ onDone }: { onDone: () => void }) {
|
||||
const [addCron, { isLoading }] = useAddCronJobMutation();
|
||||
const { data: agentsData } = useGetAgentsQuery();
|
||||
const [name, setName] = useState('');
|
||||
const [scheduleKind, setScheduleKind] = useState<ScheduleKind>('cron');
|
||||
const [scheduleValue, setScheduleValue] = useState('');
|
||||
const [atDatetime, setAtDatetime] = useState('');
|
||||
const [message, setMessage] = useState('');
|
||||
const [agent, setAgent] = useState('');
|
||||
const [session, setSession] = useState('isolated');
|
||||
const [tz, setTz] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const formik = useFormik<CronFormValues>({
|
||||
initialValues,
|
||||
onSubmit: async (values) => {
|
||||
setError('');
|
||||
const opts: Record<string, string> = {};
|
||||
if (values.name) opts.name = values.name;
|
||||
if (values.message) opts.message = values.message;
|
||||
if (values.agent) opts.agent = values.agent;
|
||||
if (values.session === 'main' || values.session === 'isolated') {
|
||||
opts.session = values.session;
|
||||
} else if (values.session) {
|
||||
opts['session-key'] = values.session;
|
||||
opts.session = `session:${values.session}`;
|
||||
}
|
||||
if (values.tz) opts.tz = values.tz;
|
||||
|
||||
if (values.scheduleKind === 'at') {
|
||||
if (!values.atDatetime) return;
|
||||
opts.at = new Date(values.atDatetime).toISOString();
|
||||
} else {
|
||||
if (!values.scheduleValue.trim()) return;
|
||||
opts[values.scheduleKind] = values.scheduleValue;
|
||||
}
|
||||
|
||||
try {
|
||||
await addCron(opts).unwrap();
|
||||
onDone();
|
||||
} catch (err: unknown) {
|
||||
const msg = (err as { data?: { error?: string } })?.data?.error;
|
||||
setError(msg || 'Failed to add cron job');
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const agents = agentsData?.items ?? [];
|
||||
const selectedAgent = agents.find((a) => (a.openclawAgentId || a.name) === agent);
|
||||
const selectedAgent = agents.find((a) => (a.openclawAgentId || a.name) === formik.values.agent);
|
||||
const { data: convData } = useGetConversationsQuery(selectedAgent?._id ?? '', {
|
||||
skip: !selectedAgent,
|
||||
});
|
||||
const conversations = convData?.items ?? [];
|
||||
|
||||
const handleSubmit = async () => {
|
||||
setError('');
|
||||
const opts: Record<string, string> = {};
|
||||
if (name) opts.name = name;
|
||||
if (message) opts.message = message;
|
||||
if (agent) opts.agent = agent;
|
||||
if (session === 'main' || session === 'isolated') {
|
||||
opts.session = session;
|
||||
} else if (session) {
|
||||
opts['session-key'] = session;
|
||||
opts.session = `session:${session}`;
|
||||
}
|
||||
if (tz) opts.tz = tz;
|
||||
|
||||
if (scheduleKind === 'at') {
|
||||
if (!atDatetime) return;
|
||||
opts.at = new Date(atDatetime).toISOString();
|
||||
} else {
|
||||
if (!scheduleValue.trim()) return;
|
||||
opts[scheduleKind] = scheduleValue;
|
||||
}
|
||||
|
||||
try {
|
||||
await addCron(opts).unwrap();
|
||||
onDone();
|
||||
} catch (err: unknown) {
|
||||
const msg = (err as { data?: { error?: string } })?.data?.error;
|
||||
setError(msg || 'Failed to add cron job');
|
||||
}
|
||||
};
|
||||
|
||||
const hasSchedule = scheduleKind === 'at' ? !!atDatetime : !!scheduleValue.trim();
|
||||
const canSubmit = hasSchedule && (name.trim() || message.trim());
|
||||
const hasSchedule =
|
||||
formik.values.scheduleKind === 'at'
|
||||
? !!formik.values.atDatetime
|
||||
: !!formik.values.scheduleValue.trim();
|
||||
const canSubmit = hasSchedule && (formik.values.name.trim() || formik.values.message.trim());
|
||||
|
||||
return (
|
||||
<Box sx={{ p: 2, mb: 2, borderRadius: 2, bgcolor: 'action.hover' }}>
|
||||
@@ -115,159 +136,163 @@ export default function AddCronForm({ onDone }: { onDone: () => void }) {
|
||||
</IconButton>
|
||||
</Box>
|
||||
|
||||
<TextField
|
||||
fullWidth
|
||||
size="small"
|
||||
label="Job Name"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
sx={inputSx}
|
||||
/>
|
||||
|
||||
<FormControl fullWidth size="small" sx={{ mb: 1.5 }}>
|
||||
<InputLabel sx={{ fontSize: '0.85rem' }}>Schedule Type</InputLabel>
|
||||
<Select
|
||||
value={scheduleKind}
|
||||
label="Schedule Type"
|
||||
onChange={(e) => {
|
||||
const kind = e.target.value as ScheduleKind;
|
||||
setScheduleKind(kind);
|
||||
if (kind === 'every') setTz('');
|
||||
}}
|
||||
sx={{ fontSize: '0.85rem' }}
|
||||
>
|
||||
{SCHEDULE_KINDS.map((s) => (
|
||||
<MenuItem key={s.value} value={s.value} sx={{ fontSize: '0.85rem' }}>
|
||||
{s.label}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
{scheduleKind === 'at' ? (
|
||||
<form onSubmit={formik.handleSubmit}>
|
||||
<TextField
|
||||
fullWidth
|
||||
size="small"
|
||||
type="datetime-local"
|
||||
label="Run At"
|
||||
value={atDatetime}
|
||||
onChange={(e) => setAtDatetime(e.target.value)}
|
||||
slotProps={{ inputLabel: { shrink: true } }}
|
||||
label="Job Name"
|
||||
{...formik.getFieldProps('name')}
|
||||
sx={inputSx}
|
||||
/>
|
||||
) : (
|
||||
<TextField
|
||||
fullWidth
|
||||
size="small"
|
||||
label={
|
||||
scheduleKind === 'cron'
|
||||
? 'Cron Expression (e.g. 0 */6 * * *)'
|
||||
: 'Interval (e.g. 30m, 2h)'
|
||||
}
|
||||
value={scheduleValue}
|
||||
onChange={(e) => setScheduleValue(e.target.value)}
|
||||
sx={inputSx}
|
||||
/>
|
||||
)}
|
||||
|
||||
<TextField
|
||||
fullWidth
|
||||
size="small"
|
||||
label="Message (agent prompt)"
|
||||
multiline
|
||||
minRows={2}
|
||||
maxRows={4}
|
||||
value={message}
|
||||
onChange={(e) => setMessage(e.target.value)}
|
||||
sx={inputSx}
|
||||
/>
|
||||
|
||||
<Box sx={{ display: 'flex', gap: 1.5, mb: 1.5 }}>
|
||||
<FormControl fullWidth size="small">
|
||||
<InputLabel sx={{ fontSize: '0.85rem' }}>Agent (optional)</InputLabel>
|
||||
<FormControl fullWidth size="small" sx={{ mb: 1.5 }}>
|
||||
<InputLabel sx={{ fontSize: '0.85rem' }}>Schedule Type</InputLabel>
|
||||
<Select
|
||||
value={agent}
|
||||
label="Agent (optional)"
|
||||
value={formik.values.scheduleKind}
|
||||
label="Schedule Type"
|
||||
onChange={(e) => {
|
||||
setAgent(e.target.value);
|
||||
setSession('isolated');
|
||||
const kind = e.target.value as ScheduleKind;
|
||||
formik.setFieldValue('scheduleKind', kind);
|
||||
if (kind === 'every') formik.setFieldValue('tz', '');
|
||||
}}
|
||||
sx={{ fontSize: '0.85rem', borderRadius: 1.5 }}
|
||||
sx={{ fontSize: '0.85rem' }}
|
||||
>
|
||||
<MenuItem value="" sx={{ fontSize: '0.85rem' }}>
|
||||
<em>None</em>
|
||||
</MenuItem>
|
||||
{agents.map((a) => (
|
||||
<MenuItem key={a._id} value={a.openclawAgentId || a.name} sx={{ fontSize: '0.85rem' }}>
|
||||
{a.name}
|
||||
{SCHEDULE_KINDS.map((s) => (
|
||||
<MenuItem key={s.value} value={s.value} sx={{ fontSize: '0.85rem' }}>
|
||||
{s.label}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
{scheduleKind !== 'every' && (
|
||||
<Autocomplete
|
||||
{formik.values.scheduleKind === 'at' ? (
|
||||
<TextField
|
||||
fullWidth
|
||||
size="small"
|
||||
freeSolo
|
||||
options={COMMON_TIMEZONES}
|
||||
value={tz || null}
|
||||
onChange={(_e, val) => setTz(val || '')}
|
||||
onInputChange={(_e, val) => setTz(val || '')}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
{...params}
|
||||
label="Timezone (optional)"
|
||||
sx={{
|
||||
'& .MuiOutlinedInput-root': { borderRadius: 1.5 },
|
||||
'& input': { fontSize: '0.85rem' },
|
||||
'& label': { fontSize: '0.85rem' },
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
type="datetime-local"
|
||||
label="Run At"
|
||||
{...formik.getFieldProps('atDatetime')}
|
||||
slotProps={{ inputLabel: { shrink: true } }}
|
||||
sx={inputSx}
|
||||
/>
|
||||
) : (
|
||||
<TextField
|
||||
fullWidth
|
||||
size="small"
|
||||
label={
|
||||
formik.values.scheduleKind === 'cron'
|
||||
? 'Cron Expression (e.g. 0 */6 * * *)'
|
||||
: 'Interval (e.g. 30m, 2h)'
|
||||
}
|
||||
{...formik.getFieldProps('scheduleValue')}
|
||||
sx={inputSx}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{agent && (
|
||||
<FormControl fullWidth size="small" sx={{ mb: 1.5 }}>
|
||||
<InputLabel sx={{ fontSize: '0.85rem' }}>Session</InputLabel>
|
||||
<Select
|
||||
value={session}
|
||||
label="Session"
|
||||
onChange={(e) => setSession(e.target.value)}
|
||||
sx={{ fontSize: '0.85rem', borderRadius: 1.5 }}
|
||||
>
|
||||
<MenuItem value="isolated" sx={{ fontSize: '0.85rem' }}>Isolated (new session)</MenuItem>
|
||||
<MenuItem value="main" sx={{ fontSize: '0.85rem' }}>Main session</MenuItem>
|
||||
{conversations
|
||||
.filter((c) => c.sessionKey)
|
||||
.map((c) => (
|
||||
<TextField
|
||||
fullWidth
|
||||
size="small"
|
||||
label="Message (agent prompt)"
|
||||
multiline
|
||||
minRows={2}
|
||||
maxRows={4}
|
||||
{...formik.getFieldProps('message')}
|
||||
sx={inputSx}
|
||||
/>
|
||||
|
||||
<Box sx={{ display: 'flex', gap: 1.5, mb: 1.5 }}>
|
||||
<FormControl fullWidth size="small">
|
||||
<InputLabel sx={{ fontSize: '0.85rem' }}>Agent (optional)</InputLabel>
|
||||
<Select
|
||||
value={formik.values.agent}
|
||||
label="Agent (optional)"
|
||||
onChange={(e) => {
|
||||
formik.setFieldValue('agent', e.target.value);
|
||||
formik.setFieldValue('session', 'isolated');
|
||||
}}
|
||||
sx={{ fontSize: '0.85rem', borderRadius: 1.5 }}
|
||||
>
|
||||
<MenuItem value="" sx={{ fontSize: '0.85rem' }}>
|
||||
<em>None</em>
|
||||
</MenuItem>
|
||||
{agents.map((a) => (
|
||||
<MenuItem
|
||||
key={c._id}
|
||||
value={`agent:${agent}:${c.sessionKey}`}
|
||||
key={a._id}
|
||||
value={a.openclawAgentId || a.name}
|
||||
sx={{ fontSize: '0.85rem' }}
|
||||
>
|
||||
{c.title || `Session ${c.sessionKey}`}
|
||||
{a.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
{error && (
|
||||
<Typography sx={{ fontSize: '0.75rem', color: 'error.main', mb: 1 }}>{error}</Typography>
|
||||
)}
|
||||
{formik.values.scheduleKind !== 'every' && (
|
||||
<Autocomplete
|
||||
fullWidth
|
||||
size="small"
|
||||
freeSolo
|
||||
options={COMMON_TIMEZONES}
|
||||
value={formik.values.tz || null}
|
||||
onChange={(_e, val) => formik.setFieldValue('tz', val || '')}
|
||||
onInputChange={(_e, val) => formik.setFieldValue('tz', val || '')}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
{...params}
|
||||
label="Timezone (optional)"
|
||||
sx={{
|
||||
'& .MuiOutlinedInput-root': { borderRadius: 1.5 },
|
||||
'& input': { fontSize: '0.85rem' },
|
||||
'& label': { fontSize: '0.85rem' },
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Button
|
||||
variant="contained"
|
||||
size="small"
|
||||
disabled={!canSubmit || isLoading}
|
||||
onClick={handleSubmit}
|
||||
sx={{ textTransform: 'none', fontSize: '0.8rem' }}
|
||||
>
|
||||
{isLoading ? <CircularProgress size={16} /> : 'Add Job'}
|
||||
</Button>
|
||||
{formik.values.agent && (
|
||||
<FormControl fullWidth size="small" sx={{ mb: 1.5 }}>
|
||||
<InputLabel sx={{ fontSize: '0.85rem' }}>Session</InputLabel>
|
||||
<Select
|
||||
value={formik.values.session}
|
||||
label="Session"
|
||||
onChange={(e) => formik.setFieldValue('session', e.target.value)}
|
||||
sx={{ fontSize: '0.85rem', borderRadius: 1.5 }}
|
||||
>
|
||||
<MenuItem value="isolated" sx={{ fontSize: '0.85rem' }}>
|
||||
Isolated (new session)
|
||||
</MenuItem>
|
||||
<MenuItem value="main" sx={{ fontSize: '0.85rem' }}>Main session</MenuItem>
|
||||
{conversations
|
||||
.filter((c) => c.sessionKey)
|
||||
.map((c) => (
|
||||
<MenuItem
|
||||
key={c._id}
|
||||
value={`agent:${formik.values.agent}:${c.sessionKey}`}
|
||||
sx={{ fontSize: '0.85rem' }}
|
||||
>
|
||||
{c.title || `Session ${c.sessionKey}`}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<Typography sx={{ fontSize: '0.75rem', color: 'error.main', mb: 1 }}>{error}</Typography>
|
||||
)}
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
variant="contained"
|
||||
size="small"
|
||||
disabled={!canSubmit || isLoading}
|
||||
sx={{ textTransform: 'none', fontSize: '0.8rem' }}
|
||||
>
|
||||
{isLoading ? <CircularProgress size={16} /> : 'Add Job'}
|
||||
</Button>
|
||||
</form>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useFormik } from 'formik';
|
||||
import { Box, Typography, IconButton, TextField, Button, CircularProgress } from '@mui/material';
|
||||
import { Close } from '@mui/icons-material';
|
||||
import {
|
||||
@@ -17,11 +18,6 @@ function parseFieldErrors(err: unknown): FieldErrors | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
function fieldError(errors: FieldErrors | null, field: string): string {
|
||||
if (!errors || !errors[field]) return '';
|
||||
return errors[field].join('. ');
|
||||
}
|
||||
|
||||
const inputSx = {
|
||||
mb: 1.5,
|
||||
'& .MuiOutlinedInput-root': { borderRadius: 1.5 },
|
||||
@@ -30,6 +26,8 @@ const inputSx = {
|
||||
'& .MuiFormHelperText-root': { fontSize: '0.7rem', mx: 0.5 },
|
||||
};
|
||||
|
||||
const emptyValues = { name: '', lastName: '', email: '', password: '', phone: '' };
|
||||
|
||||
export default function UserForm({
|
||||
userId,
|
||||
onDone,
|
||||
@@ -41,57 +39,61 @@ export default function UserForm({
|
||||
const { data: existing, isLoading: loadingUser } = useGetUserQuery(userId!, { skip: !userId });
|
||||
const [createUser, { isLoading: isCreating }] = useCreateUserMutation();
|
||||
const [updateUser, { isLoading: isUpdating }] = useUpdateUserMutation();
|
||||
|
||||
const [name, setName] = useState('');
|
||||
const [lastName, setLastName] = useState('');
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [phone, setPhone] = useState('');
|
||||
const [errors, setErrors] = useState<FieldErrors | null>(null);
|
||||
const [serverErrors, setServerErrors] = useState<FieldErrors | null>(null);
|
||||
const [generalError, setGeneralError] = useState('');
|
||||
|
||||
const formik = useFormik({
|
||||
initialValues: emptyValues,
|
||||
onSubmit: async (values) => {
|
||||
setServerErrors(null);
|
||||
setGeneralError('');
|
||||
try {
|
||||
if (isEdit && userId) {
|
||||
const data: Record<string, string> = {
|
||||
name: values.name,
|
||||
lastName: values.lastName,
|
||||
email: values.email,
|
||||
phone: values.phone,
|
||||
};
|
||||
if (values.password) data.password = values.password;
|
||||
await updateUser({ id: userId, data }).unwrap();
|
||||
} else {
|
||||
await createUser(values).unwrap();
|
||||
}
|
||||
onDone();
|
||||
} catch (err: unknown) {
|
||||
const fe = parseFieldErrors(err);
|
||||
if (fe) {
|
||||
setServerErrors(fe);
|
||||
} else {
|
||||
const msg = (err as { data?: { error?: string } })?.data?.error;
|
||||
setGeneralError(msg || (isEdit ? 'Failed to update user' : 'Failed to create user'));
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (isEdit && existing) {
|
||||
setName(existing.name || '');
|
||||
setLastName(existing.lastName || '');
|
||||
setEmail(existing.email || '');
|
||||
setPhone(existing.phone || '');
|
||||
setPassword('');
|
||||
formik.resetForm({
|
||||
values: {
|
||||
name: existing.name || '',
|
||||
lastName: existing.lastName || '',
|
||||
email: existing.email || '',
|
||||
password: '',
|
||||
phone: existing.phone || '',
|
||||
},
|
||||
});
|
||||
} else if (!isEdit) {
|
||||
setName('');
|
||||
setLastName('');
|
||||
setEmail('');
|
||||
setPassword('');
|
||||
setPhone('');
|
||||
formik.resetForm({ values: emptyValues });
|
||||
}
|
||||
setErrors(null);
|
||||
setServerErrors(null);
|
||||
setGeneralError('');
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [existing, isEdit, userId]);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
setErrors(null);
|
||||
setGeneralError('');
|
||||
try {
|
||||
if (isEdit && userId) {
|
||||
const data: Record<string, string> = { name, lastName, email, phone };
|
||||
if (password) data.password = password;
|
||||
await updateUser({ id: userId, data }).unwrap();
|
||||
} else {
|
||||
await createUser({ name, lastName, email, password, phone }).unwrap();
|
||||
}
|
||||
onDone();
|
||||
} catch (err: unknown) {
|
||||
const fe = parseFieldErrors(err);
|
||||
if (fe) {
|
||||
setErrors(fe);
|
||||
} else {
|
||||
const msg = (err as { data?: { error?: string } })?.data?.error;
|
||||
setGeneralError(msg || (isEdit ? 'Failed to update user' : 'Failed to create user'));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const saving = isCreating || isUpdating;
|
||||
const err = (field: string) => serverErrors?.[field]?.join('. ') || '';
|
||||
|
||||
return (
|
||||
<Box sx={{ p: 2, mb: 2, borderRadius: 2, bgcolor: 'action.hover' }}>
|
||||
@@ -109,26 +111,24 @@ export default function UserForm({
|
||||
<CircularProgress size={20} />
|
||||
</Box>
|
||||
) : (
|
||||
<>
|
||||
<form onSubmit={formik.handleSubmit}>
|
||||
<Box sx={{ display: 'flex', gap: 1.5 }}>
|
||||
<TextField
|
||||
fullWidth
|
||||
size="small"
|
||||
label="First Name"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
error={!!fieldError(errors, 'name')}
|
||||
helperText={fieldError(errors, 'name')}
|
||||
{...formik.getFieldProps('name')}
|
||||
error={!!err('name')}
|
||||
helperText={err('name')}
|
||||
sx={inputSx}
|
||||
/>
|
||||
<TextField
|
||||
fullWidth
|
||||
size="small"
|
||||
label="Last Name"
|
||||
value={lastName}
|
||||
onChange={(e) => setLastName(e.target.value)}
|
||||
error={!!fieldError(errors, 'lastName')}
|
||||
helperText={fieldError(errors, 'lastName')}
|
||||
{...formik.getFieldProps('lastName')}
|
||||
error={!!err('lastName')}
|
||||
helperText={err('lastName')}
|
||||
sx={inputSx}
|
||||
/>
|
||||
</Box>
|
||||
@@ -138,10 +138,9 @@ export default function UserForm({
|
||||
size="small"
|
||||
label="Email"
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
error={!!fieldError(errors, 'email')}
|
||||
helperText={fieldError(errors, 'email')}
|
||||
{...formik.getFieldProps('email')}
|
||||
error={!!err('email')}
|
||||
helperText={err('email')}
|
||||
sx={inputSx}
|
||||
/>
|
||||
|
||||
@@ -151,20 +150,18 @@ export default function UserForm({
|
||||
size="small"
|
||||
label={isEdit ? 'Password (leave empty to keep)' : 'Password'}
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
error={!!fieldError(errors, 'password')}
|
||||
helperText={fieldError(errors, 'password')}
|
||||
{...formik.getFieldProps('password')}
|
||||
error={!!err('password')}
|
||||
helperText={err('password')}
|
||||
sx={inputSx}
|
||||
/>
|
||||
<TextField
|
||||
fullWidth
|
||||
size="small"
|
||||
label="Phone"
|
||||
value={phone}
|
||||
onChange={(e) => setPhone(e.target.value)}
|
||||
error={!!fieldError(errors, 'phone')}
|
||||
helperText={fieldError(errors, 'phone')}
|
||||
{...formik.getFieldProps('phone')}
|
||||
error={!!err('phone')}
|
||||
helperText={err('phone')}
|
||||
sx={inputSx}
|
||||
/>
|
||||
</Box>
|
||||
@@ -176,15 +173,15 @@ export default function UserForm({
|
||||
)}
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
variant="contained"
|
||||
size="small"
|
||||
disabled={saving}
|
||||
onClick={handleSubmit}
|
||||
sx={{ textTransform: 'none', fontSize: '0.8rem' }}
|
||||
>
|
||||
{saving ? <CircularProgress size={16} /> : isEdit ? 'Save' : 'Create User'}
|
||||
</Button>
|
||||
</>
|
||||
</form>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "openclaw-client",
|
||||
"version": "2.3.4",
|
||||
"version": "2.3.5",
|
||||
"description": "Web-based chat interface for OpenClaw AI agents",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
Reference in New Issue
Block a user