- {ALL_CONFIGURABLE_MODULES.filter(
- module => module === currentModule
- ).map(module => {
- const fields = Object.keys(ALL_CONFIG[module]);
- return (
-
-
{module}
- {fields.map((field, index) => {
- // @ts-expect-error allow
- const { desc, type } = ALL_CONFIG[module][
- field
- ] as ConfigDescriptor;
+
+
+
{name}
+
+ {fields.map(field => {
+ let desc: string;
+ let props: ConfigInputProps;
+ if (typeof field === 'string') {
+ const descriptor = ALL_CONFIG_DESCRIPTORS[module][field];
+ desc = descriptor.desc;
+ props = {
+ type: descriptor.type,
+ defaultValue: get(appConfig[module], field),
+ field: `${module}/${field}`,
+ desc,
+ onChange: onUpdate,
+ options: [],
+ };
+ } else {
+ const descriptor = ALL_CONFIG_DESCRIPTORS[module][field.key];
- return (
-
- {index !== 0 && }
-
-
-
-
- );
- })}
-
- );
- })}
+ props = {
+ desc: field.desc ?? descriptor.desc,
+ type: field.type ?? descriptor.type,
+ // @ts-expect-error for enum type
+ options: field.options,
+ defaultValue: get(
+ appConfig[module],
+ field.key + (field.sub ? '.' + field.sub : '')
+ ),
+ field: `${module}/${field.key}${field.sub ? `/${field.sub}` : ''}`,
+ onChange: onUpdate,
+ };
+ }
+
+ return
;
+ })}
+ {operations?.map(Operation => (
+
+ ))}
+
);
diff --git a/packages/frontend/admin/src/modules/settings/operations/send-test-email.tsx b/packages/frontend/admin/src/modules/settings/operations/send-test-email.tsx
new file mode 100644
index 000000000..aab480325
--- /dev/null
+++ b/packages/frontend/admin/src/modules/settings/operations/send-test-email.tsx
@@ -0,0 +1,32 @@
+import { Button } from '@affine/admin/components/ui/button';
+import { useMutation } from '@affine/admin/use-mutation';
+import { notify } from '@affine/component';
+import type { UserFriendlyError } from '@affine/error';
+import { sendTestEmailMutation } from '@affine/graphql';
+import { useCallback } from 'react';
+
+import type { AppConfig } from '../config';
+
+export function SendTestEmail({ appConfig }: { appConfig: AppConfig }) {
+ const { trigger } = useMutation({
+ mutation: sendTestEmailMutation,
+ });
+
+ const onClick = useCallback(() => {
+ trigger(appConfig.mailer.SMTP)
+ .then(() => {
+ notify.success({
+ title: 'Test email sent',
+ message: 'The test email has been successfully sent.',
+ });
+ })
+ .catch((err: UserFriendlyError) => {
+ notify.error({
+ title: 'Failed to send test email',
+ message: err.message,
+ });
+ });
+ }, [appConfig, trigger]);
+
+ return
;
+}
diff --git a/packages/frontend/admin/src/modules/settings/runtime-setting-row.tsx b/packages/frontend/admin/src/modules/settings/runtime-setting-row.tsx
deleted file mode 100644
index c6807b48b..000000000
--- a/packages/frontend/admin/src/modules/settings/runtime-setting-row.tsx
+++ /dev/null
@@ -1,30 +0,0 @@
-import { type ReactNode } from 'react';
-
-export const RuntimeSettingRow = ({
- id,
- description,
- orientation = 'horizontal',
- children,
-}: {
- id: string;
- description: string;
- orientation?: 'horizontal' | 'vertical';
- children: ReactNode;
-}) => {
- return (
-
-
-
{description}
-
-
- {id}
-
-
-
-
{children}
-
- );
-};
diff --git a/packages/frontend/admin/src/modules/settings/use-app-config.ts b/packages/frontend/admin/src/modules/settings/use-app-config.ts
index a0f31d3c7..6db737280 100644
--- a/packages/frontend/admin/src/modules/settings/use-app-config.ts
+++ b/packages/frontend/admin/src/modules/settings/use-app-config.ts
@@ -8,11 +8,15 @@ import {
type UpdateAppConfigInput,
updateAppConfigMutation,
} from '@affine/graphql';
-import { get, merge } from 'lodash-es';
+import { cloneDeep, get, merge, set } from 'lodash-es';
import { useCallback, useState } from 'react';
+import type { AppConfig } from './config';
+
export { type UpdateAppConfigInput };
+export type AppConfigUpdates = Record
;
+
export const useAppConfig = () => {
const {
data: { appConfig },
@@ -25,49 +29,81 @@ export const useAppConfig = () => {
mutation: updateAppConfigMutation,
});
- const [updates, setUpdates] = useState<
- Record
- >({});
-
- const save = useAsyncCallback(
- async (updates: UpdateAppConfigInput[]) => {
- try {
- const savedUpdates = await trigger({
- updates,
- });
- await mutate({ appConfig: merge({}, appConfig, savedUpdates) });
- setUpdates({});
- notify.success({
- title: 'Saved successfully',
- message: 'Runtime configurations have been saved successfully.',
- });
- } catch (e) {
- const error = UserFriendlyError.fromAny(e);
- notify.error({
- title: 'Failed to save',
- message: error.message,
- });
- console.error(e);
- }
- },
- [appConfig, mutate, trigger]
+ const [updates, setUpdates] = useState({});
+ const [patchedAppConfig, setPatchedAppConfig] = useState(() =>
+ cloneDeep(appConfig)
);
+ const save = useAsyncCallback(async () => {
+ const updateInputs: UpdateAppConfigInput[] = Object.entries(updates).map(
+ ([key, value]) => {
+ const splitIndex = key.indexOf('.');
+ const module = key.slice(0, splitIndex);
+ const field = key.slice(splitIndex + 1);
+
+ return {
+ module,
+ key: field,
+ value: value.to,
+ };
+ }
+ );
+
+ try {
+ const savedUpdates = await trigger({
+ updates: updateInputs,
+ });
+ await mutate(prev => {
+ return { appConfig: merge({}, prev, savedUpdates) };
+ });
+ setUpdates({});
+ notify.success({
+ title: 'Saved',
+ message: 'Settings have been saved successfully.',
+ });
+ } catch (e) {
+ const error = UserFriendlyError.fromAny(e);
+ notify.error({
+ title: 'Failed to save',
+ message: error.message,
+ });
+ console.error(e);
+ }
+ }, [updates, mutate, trigger]);
+
const update = useCallback(
- (module: string, field: string, value: any) => {
- setUpdates(prev => ({
- ...prev,
- [`${module}.${field}`]: {
- from: get(appConfig, `${module}.${field}`),
- to: value,
- },
- }));
+ (path: string, value: any) => {
+ const [module, field, subField] = path.split('/');
+ const key = `${module}.${field}`;
+ const from = get(appConfig, key);
+ setUpdates(prev => {
+ const to = subField
+ ? set(prev[key]?.to ?? { ...from }, subField, value)
+ : value;
+
+ return {
+ ...prev,
+ [key]: {
+ from,
+ to,
+ },
+ };
+ });
+
+ setPatchedAppConfig(prev => {
+ return set(
+ prev,
+ `${module}.${field}${subField ? `.${subField}` : ''}`,
+ value
+ );
+ });
},
[appConfig]
);
return {
- appConfig,
+ appConfig: appConfig as AppConfig,
+ patchedAppConfig,
update,
save,
updates,