snapshot of https://github.com/toeverything/AFFiNE.git (canary, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Build & Test / Analyze (javascript, affine) (push) Failing after 4s
Build & Test / Analyze (typescript, affine) (push) Failing after 3s
Build & Test / Rust test filter (push) Failing after 4s
Build & Test / Check yarn binary (push) Failing after 3s
Build & Test / E2E BlockSuite Test (3) (push) Failing after 3s
Build & Test / Analyze (javascript, blocksuite) (push) Failing after 3s
Build & Test / Analyze (typescript, blocksuite) (push) Failing after 3s
Build & Test / Mobile native build filter (push) Failing after 3s
Build & Test / E2E BlockSuite Test (1) (push) Failing after 4s
Build & Test / E2E BlockSuite Test (2) (push) Failing after 4s
Build & Test / E2E Mobile Test (1) (push) Failing after 3s
Build & Test / Unit Test (1) (push) Has been skipped
Build & Test / Unit Test (2) (push) Has been skipped
Build & Test / Unit Test (3) (push) Has been skipped
Build & Test / Unit Test (4) (push) Has been skipped
Build & Test / Unit Test (5) (push) Has been skipped
Build & Test / Native Unit Test (push) Has been skipped
Build & Test / E2E BlockSuite Test (4) (push) Failing after 3s
Build & Test / E2E BlockSuite Test (5) (push) Failing after 3s
Build & Test / E2E BlockSuite Cross Browser Test (push) Failing after 3s
Build & Test / E2E Mobile Test (2) (push) Failing after 3s
Build & Test / Build AFFiNE native (x86_64-unknown-linux-gnu) (push) Failing after 3s
Build & Test / Build Server native (push) Failing after 3s
Build & Test / Check Git Status (push) Has been skipped
Build & Test / Server Test (0, 4) (push) Has been skipped
Build & Test / Server Test (1, 4) (push) Has been skipped
Build & Test / Server Test (2, 4) (push) Has been skipped
Build & Test / Server Test (3, 4) (push) Has been skipped
Build & Test / Server Test with Elasticsearch (push) Has been skipped
Build & Test / Build @affine/electron renderer (push) Failing after 3s
Build & Test / Copilot test filter (push) Failing after 4s
Build & Test / Lint Rust (push) Has been skipped
Build & Test / Server Test with Embedded Search (push) Has been skipped
Build & Test / Server Test with Manticore Search (push) Has been skipped
Build & Test / Server E2E Test (push) Has been skipped
Build & Test / Cloud Desktop E2E Test (push) Has been skipped
Build & Test / Cloud E2E Test 1/2 (push) Has been skipped
Build & Test / Cloud E2E Test 2/2 (push) Has been skipped
Build & Test / Build Android app (push) Has been skipped
Build & Test / Test Android app (push) Has been skipped
Build & Test / Run native tests (push) Has been skipped
Build & Test / Server Copilot Api Test (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (1, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (2, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (3, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (4, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (5, 5) (push) Has been skipped
Release / Canary Gate (push) Has been skipped
Build & Test / Build AFFiNE native (aarch64-apple-darwin) (push) Has been cancelled
Build & Test / Lint (push) Has been cancelled
Build & Test / Typecheck (push) Has been cancelled
Build & Test / Build AFFiNE native (x86_64-apple-darwin) (push) Has been cancelled
Build & Test / Desktop Test (ubuntu-latest, linux, x64, x86_64-unknown-linux-gnu, true) (push) Has been cancelled
Build & Test / Desktop Test (windows-latest, windows, x64, x86_64-pc-windows-msvc, true) (push) Has been cancelled
Build & Test / E2E Test (1) (push) Has been cancelled
Build & Test / E2E Test (2) (push) Has been cancelled
Build & Test / E2E Test (3) (push) Has been cancelled
Build & Test / E2E Test (4) (push) Has been cancelled
Build & Test / E2E Test (5) (push) Has been cancelled
Build & Test / Build AFFiNE native (aarch64-pc-windows-msvc) (push) Has been cancelled
Build & Test / Build AFFiNE native (x86_64-pc-windows-msvc) (push) Has been cancelled
Build & Test / Desktop Test (macos-latest, macos, arm64, aarch64-apple-darwin, true) (push) Has been cancelled
Build & Test / Desktop Test (macos-latest, macos, x64, x86_64-apple-darwin, false) (push) Has been cancelled
Build & Test / 3, 2, 1 Launch (push) Has been cancelled
Build & Test / Build iOS app (push) Has been cancelled
Copilot Test Automatically / Setup Test (push) Failing after 1s
Release / Prepare (push) Failing after 2s
Release / Release Cloud (push) Has been skipped
Release / Release Desktop (push) Has been skipped
Release / Release Docker Image (push) Has been skipped
Release / Release Mobile (push) Has been skipped
Some checks failed
Build & Test / Analyze (javascript, affine) (push) Failing after 4s
Build & Test / Analyze (typescript, affine) (push) Failing after 3s
Build & Test / Rust test filter (push) Failing after 4s
Build & Test / Check yarn binary (push) Failing after 3s
Build & Test / E2E BlockSuite Test (3) (push) Failing after 3s
Build & Test / Analyze (javascript, blocksuite) (push) Failing after 3s
Build & Test / Analyze (typescript, blocksuite) (push) Failing after 3s
Build & Test / Mobile native build filter (push) Failing after 3s
Build & Test / E2E BlockSuite Test (1) (push) Failing after 4s
Build & Test / E2E BlockSuite Test (2) (push) Failing after 4s
Build & Test / E2E Mobile Test (1) (push) Failing after 3s
Build & Test / Unit Test (1) (push) Has been skipped
Build & Test / Unit Test (2) (push) Has been skipped
Build & Test / Unit Test (3) (push) Has been skipped
Build & Test / Unit Test (4) (push) Has been skipped
Build & Test / Unit Test (5) (push) Has been skipped
Build & Test / Native Unit Test (push) Has been skipped
Build & Test / E2E BlockSuite Test (4) (push) Failing after 3s
Build & Test / E2E BlockSuite Test (5) (push) Failing after 3s
Build & Test / E2E BlockSuite Cross Browser Test (push) Failing after 3s
Build & Test / E2E Mobile Test (2) (push) Failing after 3s
Build & Test / Build AFFiNE native (x86_64-unknown-linux-gnu) (push) Failing after 3s
Build & Test / Build Server native (push) Failing after 3s
Build & Test / Check Git Status (push) Has been skipped
Build & Test / Server Test (0, 4) (push) Has been skipped
Build & Test / Server Test (1, 4) (push) Has been skipped
Build & Test / Server Test (2, 4) (push) Has been skipped
Build & Test / Server Test (3, 4) (push) Has been skipped
Build & Test / Server Test with Elasticsearch (push) Has been skipped
Build & Test / Build @affine/electron renderer (push) Failing after 3s
Build & Test / Copilot test filter (push) Failing after 4s
Build & Test / Lint Rust (push) Has been skipped
Build & Test / Server Test with Embedded Search (push) Has been skipped
Build & Test / Server Test with Manticore Search (push) Has been skipped
Build & Test / Server E2E Test (push) Has been skipped
Build & Test / Cloud Desktop E2E Test (push) Has been skipped
Build & Test / Cloud E2E Test 1/2 (push) Has been skipped
Build & Test / Cloud E2E Test 2/2 (push) Has been skipped
Build & Test / Build Android app (push) Has been skipped
Build & Test / Test Android app (push) Has been skipped
Build & Test / Run native tests (push) Has been skipped
Build & Test / Server Copilot Api Test (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (1, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (2, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (3, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (4, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (5, 5) (push) Has been skipped
Release / Canary Gate (push) Has been skipped
Build & Test / Build AFFiNE native (aarch64-apple-darwin) (push) Has been cancelled
Build & Test / Lint (push) Has been cancelled
Build & Test / Typecheck (push) Has been cancelled
Build & Test / Build AFFiNE native (x86_64-apple-darwin) (push) Has been cancelled
Build & Test / Desktop Test (ubuntu-latest, linux, x64, x86_64-unknown-linux-gnu, true) (push) Has been cancelled
Build & Test / Desktop Test (windows-latest, windows, x64, x86_64-pc-windows-msvc, true) (push) Has been cancelled
Build & Test / E2E Test (1) (push) Has been cancelled
Build & Test / E2E Test (2) (push) Has been cancelled
Build & Test / E2E Test (3) (push) Has been cancelled
Build & Test / E2E Test (4) (push) Has been cancelled
Build & Test / E2E Test (5) (push) Has been cancelled
Build & Test / Build AFFiNE native (aarch64-pc-windows-msvc) (push) Has been cancelled
Build & Test / Build AFFiNE native (x86_64-pc-windows-msvc) (push) Has been cancelled
Build & Test / Desktop Test (macos-latest, macos, arm64, aarch64-apple-darwin, true) (push) Has been cancelled
Build & Test / Desktop Test (macos-latest, macos, x64, x86_64-apple-darwin, false) (push) Has been cancelled
Build & Test / 3, 2, 1 Launch (push) Has been cancelled
Build & Test / Build iOS app (push) Has been cancelled
Copilot Test Automatically / Setup Test (push) Failing after 1s
Release / Prepare (push) Failing after 2s
Release / Release Cloud (push) Has been skipped
Release / Release Desktop (push) Has been skipped
Release / Release Docker Image (push) Has been skipped
Release / Release Mobile (push) Has been skipped
This commit is contained in:
@@ -0,0 +1,333 @@
|
||||
import { Button } from '@affine/admin/components/ui/button';
|
||||
import { Input } from '@affine/admin/components/ui/input';
|
||||
import { Label } from '@affine/admin/components/ui/label';
|
||||
import { Separator } from '@affine/admin/components/ui/separator';
|
||||
import type { FeatureType } from '@affine/graphql';
|
||||
import { ChevronRightIcon } from 'lucide-react';
|
||||
import type { ChangeEvent } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
import { FeatureToggleList } from '../../../components/shared/feature-toggle-list';
|
||||
import { useServerConfig } from '../../common';
|
||||
import { RightPanelHeader } from '../../header';
|
||||
import type { UserInput, UserType } from '../schema';
|
||||
import { validateEmails, validatePassword } from '../utils/csv-utils';
|
||||
import { useCreateUser, useUpdateUser } from './use-user-management';
|
||||
|
||||
type UserFormProps = {
|
||||
title: string;
|
||||
defaultValue?: Partial<UserInput>;
|
||||
onClose: () => void;
|
||||
onConfirm: (user: UserInput) => void;
|
||||
onValidate: (user: Partial<UserInput>) => boolean;
|
||||
actions?: React.ReactNode;
|
||||
showOption?: boolean;
|
||||
onDirtyChange?: (dirty: boolean) => void;
|
||||
};
|
||||
|
||||
function UserForm({
|
||||
title,
|
||||
defaultValue,
|
||||
onClose,
|
||||
onConfirm,
|
||||
onValidate,
|
||||
actions,
|
||||
showOption,
|
||||
onDirtyChange,
|
||||
}: UserFormProps) {
|
||||
const serverConfig = useServerConfig();
|
||||
|
||||
const defaultUser: Partial<UserInput> = useMemo(
|
||||
() => ({
|
||||
name: defaultValue?.name ?? '',
|
||||
email: defaultValue?.email ?? '',
|
||||
password: defaultValue?.password ?? '',
|
||||
features: defaultValue?.features ?? [],
|
||||
}),
|
||||
[defaultValue]
|
||||
);
|
||||
|
||||
const [changes, setChanges] = useState<Partial<UserInput>>(defaultUser);
|
||||
|
||||
const setField = useCallback(
|
||||
<K extends keyof UserInput>(
|
||||
field: K,
|
||||
value: UserInput[K] | ((prev: UserInput[K] | undefined) => UserInput[K])
|
||||
) => {
|
||||
setChanges(changes => ({
|
||||
...changes,
|
||||
[field]:
|
||||
typeof value === 'function' ? value(changes[field] as any) : value,
|
||||
}));
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const canSave = useMemo(() => {
|
||||
return onValidate(changes);
|
||||
}, [onValidate, changes]);
|
||||
|
||||
useEffect(() => {
|
||||
const normalize = (value: Partial<UserInput>) => ({
|
||||
name: value.name ?? '',
|
||||
email: value.email ?? '',
|
||||
password: value.password ?? '',
|
||||
features: [...(value.features ?? [])].sort(),
|
||||
});
|
||||
const current = normalize(changes);
|
||||
const baseline = normalize(defaultUser);
|
||||
const dirty =
|
||||
(current.name !== baseline.name ||
|
||||
current.email !== baseline.email ||
|
||||
current.password !== baseline.password ||
|
||||
current.features.join(',') !== baseline.features.join(',')) &&
|
||||
!!onDirtyChange;
|
||||
onDirtyChange?.(dirty);
|
||||
}, [changes, defaultUser, onDirtyChange]);
|
||||
|
||||
const handleConfirm = useCallback(() => {
|
||||
if (!canSave) {
|
||||
return;
|
||||
}
|
||||
|
||||
// @ts-expect-error checked
|
||||
onConfirm(changes);
|
||||
setChanges(defaultUser);
|
||||
}, [canSave, changes, defaultUser, onConfirm]);
|
||||
|
||||
const handleFeaturesChange = useCallback(
|
||||
(features: FeatureType[]) => {
|
||||
setField('features', features);
|
||||
},
|
||||
[setField]
|
||||
);
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
setChanges(defaultUser);
|
||||
onClose();
|
||||
}, [defaultUser, onClose]);
|
||||
|
||||
useEffect(() => {
|
||||
setChanges(defaultUser);
|
||||
}, [defaultUser]);
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col bg-background">
|
||||
<RightPanelHeader
|
||||
title={title}
|
||||
handleClose={handleClose}
|
||||
handleConfirm={handleConfirm}
|
||||
canSave={canSave}
|
||||
/>
|
||||
<div className="flex-grow space-y-3 overflow-y-auto p-4">
|
||||
<div className="flex flex-col rounded-xl border border-border bg-card shadow-sm">
|
||||
<InputItem
|
||||
label="User name"
|
||||
field="name"
|
||||
value={changes.name}
|
||||
onChange={setField}
|
||||
placeholder="Enter user name"
|
||||
/>
|
||||
<Separator />
|
||||
<InputItem
|
||||
label="Email"
|
||||
field="email"
|
||||
value={changes.email}
|
||||
onChange={setField}
|
||||
placeholder="Enter email address"
|
||||
/>
|
||||
{showOption && (
|
||||
<>
|
||||
<Separator />
|
||||
<InputItem
|
||||
label="Password"
|
||||
field="password"
|
||||
value={changes.password}
|
||||
onChange={setField}
|
||||
optional
|
||||
placeholder="Enter password"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<FeatureToggleList
|
||||
className="rounded-xl border border-border bg-card shadow-sm"
|
||||
features={serverConfig.availableUserFeatures}
|
||||
selected={changes.features ?? []}
|
||||
onChange={handleFeaturesChange}
|
||||
control="switch"
|
||||
controlPosition="right"
|
||||
showSeparators={true}
|
||||
/>
|
||||
{actions}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function InputItem({
|
||||
label,
|
||||
field,
|
||||
optional,
|
||||
value,
|
||||
onChange,
|
||||
placeholder,
|
||||
}: {
|
||||
label: string;
|
||||
field: keyof UserInput;
|
||||
optional?: boolean;
|
||||
value?: string;
|
||||
onChange: (field: keyof UserInput, value: string) => void;
|
||||
placeholder?: string;
|
||||
}) {
|
||||
const onValueChange = useCallback(
|
||||
(e: ChangeEvent<HTMLInputElement>) => {
|
||||
onChange(field, e.target.value);
|
||||
},
|
||||
[field, onChange]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2 p-3">
|
||||
<Label className="flex flex-wrap text-xs font-medium leading-5 text-muted-foreground uppercase tracking-wide">
|
||||
{label}
|
||||
{optional && (
|
||||
<span className="ml-1 font-normal text-muted-foreground">
|
||||
(optional)
|
||||
</span>
|
||||
)}
|
||||
</Label>
|
||||
<Input
|
||||
type="text"
|
||||
className="py-2 px-3 text-sm font-normal h-9"
|
||||
value={value}
|
||||
onChange={onValueChange}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const validateCreateUser = (user: Partial<UserInput>) => {
|
||||
return !!user.name && !!user.email && !!user.features;
|
||||
};
|
||||
|
||||
const validateUpdateUser = (user: Partial<UserInput>) => {
|
||||
return !!user.name || !!user.email;
|
||||
};
|
||||
|
||||
export function CreateUserForm({
|
||||
onComplete,
|
||||
onDirtyChange,
|
||||
}: {
|
||||
onComplete: () => void;
|
||||
onDirtyChange?: (dirty: boolean) => void;
|
||||
}) {
|
||||
const { create, creating } = useCreateUser();
|
||||
const serverConfig = useServerConfig();
|
||||
const passwordLimits = serverConfig.credentialsRequirement.password;
|
||||
useEffect(() => {
|
||||
if (creating) {
|
||||
return () => {
|
||||
onComplete();
|
||||
};
|
||||
}
|
||||
|
||||
return;
|
||||
}, [creating, onComplete]);
|
||||
|
||||
const handleCreateUser = useCallback(
|
||||
(user: UserInput) => {
|
||||
const emailValidation = validateEmails([user]);
|
||||
const passwordValidation = validatePassword(
|
||||
user.password,
|
||||
passwordLimits
|
||||
);
|
||||
if (!passwordValidation.valid || !emailValidation[0].valid) {
|
||||
toast.error(passwordValidation.error || emailValidation[0].error);
|
||||
return;
|
||||
}
|
||||
create(user);
|
||||
},
|
||||
[create, passwordLimits]
|
||||
);
|
||||
|
||||
return (
|
||||
<UserForm
|
||||
title="Create User"
|
||||
onClose={onComplete}
|
||||
onConfirm={handleCreateUser}
|
||||
onValidate={validateCreateUser}
|
||||
showOption={true}
|
||||
onDirtyChange={onDirtyChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function UpdateUserForm({
|
||||
user,
|
||||
onResetPassword,
|
||||
onDeleteAccount,
|
||||
onComplete,
|
||||
onDirtyChange,
|
||||
}: {
|
||||
user: UserType;
|
||||
onResetPassword: () => void;
|
||||
onDeleteAccount: () => void;
|
||||
onComplete: () => void;
|
||||
onDirtyChange?: (dirty: boolean) => void;
|
||||
}) {
|
||||
const { update, updating } = useUpdateUser();
|
||||
|
||||
const onUpdateUser = useCallback(
|
||||
(updates: UserInput) => {
|
||||
update({
|
||||
...updates,
|
||||
userId: user.id,
|
||||
});
|
||||
},
|
||||
[user, update]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (updating) {
|
||||
return () => {
|
||||
onComplete();
|
||||
};
|
||||
}
|
||||
return;
|
||||
}, [updating, onComplete]);
|
||||
|
||||
return (
|
||||
<UserForm
|
||||
title="Update User"
|
||||
defaultValue={user}
|
||||
onClose={onComplete}
|
||||
onConfirm={onUpdateUser}
|
||||
onValidate={validateUpdateUser}
|
||||
onDirtyChange={onDirtyChange}
|
||||
actions={
|
||||
<div className="space-y-2">
|
||||
<Button
|
||||
className="h-10 w-full justify-between rounded-xl border-border/60 px-4 text-sm font-medium hover:bg-muted/50"
|
||||
variant="outline"
|
||||
onClick={onResetPassword}
|
||||
>
|
||||
<span>Reset Password</span>
|
||||
<ChevronRightIcon size={16} className="text-muted-foreground" />
|
||||
</Button>
|
||||
<Button
|
||||
className="h-10 w-full justify-between rounded-xl border-destructive/30 px-4 text-sm font-medium text-destructive hover:bg-destructive/5 hover:text-destructive"
|
||||
variant="outline"
|
||||
onClick={onDeleteAccount}
|
||||
>
|
||||
<span>Delete Account</span>
|
||||
<ChevronRightIcon size={16} />
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user