From 3c6151b806e7c5ecf011d49f092b6adbd89743fe Mon Sep 17 00:00:00 2001 From: robin Date: Wed, 7 Dec 2022 18:15:27 +0800 Subject: [PATCH 1/6] feat(ui): admin module update --- i18n/en_US.yaml | 62 ++++++++ ui/src/common/constants.ts | 12 ++ ui/src/components/SchemaForm/index.tsx | 71 ++++++--- ui/src/hooks/index.ts | 4 + ui/src/hooks/useChangePasswordModal/index.tsx | 130 +++++++++++++++ ui/src/hooks/useUserModal/index.tsx | 150 ++++++++++++++++++ ui/src/pages/Admin/CssAndHtml/index.tsx | 119 ++++++++++++++ ui/src/pages/Admin/Login/index.tsx | 93 +++++++++++ ui/src/pages/Admin/Themes/index.tsx | 105 ++++++++++++ ui/src/pages/Admin/Users/index.tsx | 37 ++++- ui/src/router/routes.ts | 12 ++ 11 files changed, 766 insertions(+), 29 deletions(-) create mode 100644 ui/src/hooks/useChangePasswordModal/index.tsx create mode 100644 ui/src/hooks/useUserModal/index.tsx create mode 100644 ui/src/pages/Admin/CssAndHtml/index.tsx create mode 100644 ui/src/pages/Admin/Login/index.tsx create mode 100644 ui/src/pages/Admin/Themes/index.tsx diff --git a/i18n/en_US.yaml b/i18n/en_US.yaml index 71b8cb16..c67e1be5 100644 --- a/i18n/en_US.yaml +++ b/i18n/en_US.yaml @@ -976,6 +976,10 @@ ui: tos: Terms of Service privacy: Privacy seo: SEO + customize: Customize + themes: Themes + css_and_html: CSS/HTML + login: Login admin: admin_header: title: Admin @@ -1079,6 +1083,29 @@ ui: change_status: Change status change_role: Change role show_logs: Show logs + add_user: Add user + new_password_modal: + title: Set new password + form: + fields: + password: + label: Password + btn_cancel: Cancel + btn_submit: Submit + user_modal: + title: Add new user + form: + fields: + display_name: + label: Display Name + email: + label: Email + msg: Email is not valid. + password: + label: Password + btn_cancel: Cancel + btn_submit: Submit + questions: page_title: Questions normal: Normal @@ -1230,6 +1257,41 @@ ui: robots: label: robots.txt text: This will permanently override any related site settings. + themes: + page_title: Themes + themes: + label: Themes + text: Select an existing theme. + navbar_style: + label: Navbar Style + text: Select an existing theme. + primary_color: + label: Primary Color + text: Modify the colors used by your themes + css_and_html: + page_title: CSS and HTML + custom_css: + label: Custom CSS + text: This will insert as + head: + label: Head + text: This will insert before + header: + label: Header + text: This will insert after + footer: + label: Footer + text: This will insert before . + login: + page_title: Login + membership: + label: Membership + labelAlias: Allow new registrations + text: Turn off to prevent anyone from creating a new account. + private: + label: Private + text: Only logged in users can access this community. + form: empty: cannot be empty invalid: is invalid diff --git a/ui/src/common/constants.ts b/ui/src/common/constants.ts index 90a07fd6..067bb88b 100644 --- a/ui/src/common/constants.ts +++ b/ui/src/common/constants.ts @@ -53,6 +53,17 @@ export const ADMIN_NAV_MENUS = [ name: 'flags', // badgeContent: 5, }, + { + name: 'customize', + children: [ + { + name: 'themes', + }, + { + name: 'css_and_html', + }, + ], + }, { name: 'settings', children: [ @@ -63,6 +74,7 @@ export const ADMIN_NAV_MENUS = [ { name: 'legal' }, { name: 'write' }, { name: 'seo' }, + { name: 'login' }, ], }, ]; diff --git a/ui/src/components/SchemaForm/index.tsx b/ui/src/components/SchemaForm/index.tsx index f71f4476..73a94407 100644 --- a/ui/src/components/SchemaForm/index.tsx +++ b/ui/src/components/SchemaForm/index.tsx @@ -1,4 +1,8 @@ -import { FC } from 'react'; +import { + ForwardRefRenderFunction, + forwardRef, + useImperativeHandle, +} from 'react'; import { Form, Button, Stack } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; @@ -69,8 +73,13 @@ interface IProps { schema: JSONSchema; uiSchema?: UISchema; formData?: Type.FormDataType; + hiddenSubmit?: boolean; onChange?: (data: Type.FormDataType) => void; - onSubmit: (e: React.FormEvent) => void; + onSubmit?: (e: React.FormEvent) => void; +} + +interface IRef { + validator: () => Promise; } /** @@ -81,13 +90,17 @@ interface IProps { * @param onChange change event * @param onSubmit submit event */ -const SchemaForm: FC = ({ - schema, - uiSchema = {}, - formData = {}, - onChange, - onSubmit, -}) => { +const SchemaForm: ForwardRefRenderFunction = ( + { + schema, + uiSchema = {}, + formData = {}, + onChange, + onSubmit, + hiddenSubmit = false, + }, + ref, +) => { const { t } = useTranslation('translation', { keyPrefix: 'form', }); @@ -188,9 +201,7 @@ const SchemaForm: FC = ({ ); }; - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - + const validator = async (): Promise => { const errors = requiredValidator(); if (errors.length > 0) { formData = errors.reduce((acc, cur) => { @@ -207,7 +218,7 @@ const SchemaForm: FC = ({ if (onChange instanceof Function) { onChange({ ...formData }); } - return; + return false; } const syncErrors = await syncValidator(); if (syncErrors.length > 0) { @@ -223,8 +234,18 @@ const SchemaForm: FC = ({ if (onChange instanceof Function) { onChange({ ...formData }); } + return false; + } + return true; + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + const isValid = await validator(); + if (!isValid) { return; } + Object.keys(formData).forEach((key) => { formData[key].isInvalid = false; formData[key].errorMsg = ''; @@ -232,7 +253,9 @@ const SchemaForm: FC = ({ if (onChange instanceof Function) { onChange(formData); } - onSubmit(e); + if (onSubmit instanceof Function) { + onSubmit(e); + } }; const handleUploadChange = (name: string, value: string) => { @@ -260,6 +283,10 @@ const SchemaForm: FC = ({ } }; + useImperativeHandle(ref, () => ({ + validator, + })); + return (
{keys.map((key) => { @@ -312,7 +339,7 @@ const SchemaForm: FC = ({ required type={widget} name={key} - id={String(item)} + id={`form-${String(item)}`} label={properties[key].enumNames?.[index]} checked={formData[key]?.value === item} feedback={formData[key]?.errorMsg} @@ -342,7 +369,7 @@ const SchemaForm: FC = ({ {title} = ({ if (widget === 'textarea') { return ( {title} @@ -468,9 +495,11 @@ const SchemaForm: FC = ({ ); })} - + {!hiddenSubmit && ( + + )} ); }; @@ -488,4 +517,4 @@ export const initFormData = (schema: JSONSchema): Type.FormDataType => { return formData; }; -export default SchemaForm; +export default forwardRef(SchemaForm); diff --git a/ui/src/hooks/index.ts b/ui/src/hooks/index.ts index 511f8012..a741976a 100644 --- a/ui/src/hooks/index.ts +++ b/ui/src/hooks/index.ts @@ -6,6 +6,8 @@ import useChangeModal from './useChangeModal'; import useEditStatusModal from './useEditStatusModal'; import useChangeUserRoleModal from './useChangeUserRoleModal'; import useHeadInfo from './useHeadInfo'; +import useUserModal from './useUserModal'; +import useChangePasswordModal from './useChangePasswordModal'; export { useTagModal, @@ -16,4 +18,6 @@ export { useEditStatusModal, useChangeUserRoleModal, useHeadInfo, + useUserModal, + useChangePasswordModal, }; diff --git a/ui/src/hooks/useChangePasswordModal/index.tsx b/ui/src/hooks/useChangePasswordModal/index.tsx new file mode 100644 index 00000000..ba0b7325 --- /dev/null +++ b/ui/src/hooks/useChangePasswordModal/index.tsx @@ -0,0 +1,130 @@ +import { useLayoutEffect, useState, useRef } from 'react'; +import { Modal, Button } from 'react-bootstrap'; +import { useTranslation } from 'react-i18next'; + +import ReactDOM from 'react-dom/client'; + +import type * as Type from '@/common/interface'; +import { SchemaForm, JSONSchema, UISchema, initFormData } from '@/components'; + +const div = document.createElement('div'); +const root = ReactDOM.createRoot(div); + +interface IProps { + title?: string; + onConfirm?: (formData: any) => void; +} +const useChangePasswordModal = (props: IProps = {}) => { + const { t } = useTranslation('translation', { + keyPrefix: 'admin.users.new_password_modal', + }); + + const { title = t('title'), onConfirm } = props; + const [visible, setVisibleState] = useState(false); + const schema: JSONSchema = { + title: t('title'), + required: ['password'], + properties: { + new_password: { + type: 'string', + title: t('form.fields.password.label'), + }, + }, + }; + const uiSchema: UISchema = { + new_password: { + 'ui:options': { + type: 'password', + }, + }, + }; + const [formData, setFormData] = useState( + initFormData(schema), + ); + + const formRef = useRef<{ + validator: () => Promise; + }>(null); + + const onClose = () => { + setVisibleState(false); + }; + + const onShow = () => { + setVisibleState(true); + }; + + const handleSubmit = async (event) => { + event.preventDefault(); + event.stopPropagation(); + const isValid = await formRef.current?.validator(); + + if (!isValid) { + return; + } + + if (onConfirm instanceof Function) { + onConfirm({ + slug_name: formData.slugName.value, + display_name: formData.displayName.value, + original_text: formData.description.value, + }); + setFormData({ + displayName: { + value: '', + isInvalid: false, + errorMsg: '', + }, + slugName: { + value: '', + isInvalid: false, + errorMsg: '', + }, + description: { + value: '', + isInvalid: false, + errorMsg: '', + }, + }); + } + onClose(); + }; + + const handleOnChange = (data) => { + setFormData(data); + }; + + useLayoutEffect(() => { + root.render( + + + {title} + + + + + + + + + , + ); + }); + return { + onClose, + onShow, + }; +}; + +export default useChangePasswordModal; diff --git a/ui/src/hooks/useUserModal/index.tsx b/ui/src/hooks/useUserModal/index.tsx new file mode 100644 index 00000000..713a2caa --- /dev/null +++ b/ui/src/hooks/useUserModal/index.tsx @@ -0,0 +1,150 @@ +import { useLayoutEffect, useState, useRef } from 'react'; +import { Modal, Button } from 'react-bootstrap'; +import { useTranslation } from 'react-i18next'; + +import ReactDOM from 'react-dom/client'; + +import pattern from '@/common/pattern'; +import type * as Type from '@/common/interface'; +import { SchemaForm, JSONSchema, UISchema, initFormData } from '@/components'; + +const div = document.createElement('div'); +const root = ReactDOM.createRoot(div); + +interface IProps { + title?: string; + onConfirm?: (formData: any) => void; +} +const useAddUserModal = (props: IProps = {}) => { + const { t } = useTranslation('translation', { + keyPrefix: 'admin.users.user_modal', + }); + + const { title = t('title'), onConfirm } = props; + const [visible, setVisibleState] = useState(false); + const schema: JSONSchema = { + title: t('title'), + required: ['display_name', 'email', 'password'], + properties: { + display_name: { + type: 'string', + title: t('form.fields.display_name.label'), + }, + email: { + type: 'string', + title: t('form.fields.email.label'), + }, + password: { + type: 'string', + title: t('form.fields.password.label'), + }, + }, + }; + const uiSchema: UISchema = { + email: { + 'ui:options': { + type: 'email', + validator: (value) => { + if (value && !pattern.email.test(value)) { + return t('form.fields.email.msg'); + } + return true; + }, + }, + }, + password: { + 'ui:options': { + type: 'password', + }, + }, + }; + const [formData, setFormData] = useState( + initFormData(schema), + ); + + const formRef = useRef<{ + validator: () => Promise; + }>(null); + + const onClose = () => { + setVisibleState(false); + }; + + const onShow = () => { + setVisibleState(true); + }; + + const handleSubmit = async (event) => { + event.preventDefault(); + event.stopPropagation(); + const isValid = await formRef.current?.validator(); + + if (!isValid) { + return; + } + + if (onConfirm instanceof Function) { + onConfirm({ + slug_name: formData.slugName.value, + display_name: formData.displayName.value, + original_text: formData.description.value, + }); + setFormData({ + displayName: { + value: '', + isInvalid: false, + errorMsg: '', + }, + slugName: { + value: '', + isInvalid: false, + errorMsg: '', + }, + description: { + value: '', + isInvalid: false, + errorMsg: '', + }, + }); + } + onClose(); + }; + + const handleOnChange = (data) => { + setFormData(data); + }; + + useLayoutEffect(() => { + root.render( + + + {title} + + + + + + + + + , + ); + }); + return { + onClose, + onShow, + }; +}; + +export default useAddUserModal; diff --git a/ui/src/pages/Admin/CssAndHtml/index.tsx b/ui/src/pages/Admin/CssAndHtml/index.tsx new file mode 100644 index 00000000..a06b8060 --- /dev/null +++ b/ui/src/pages/Admin/CssAndHtml/index.tsx @@ -0,0 +1,119 @@ +import { FC, useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; + +import type * as Type from '@/common/interface'; +import { getSeoSetting, putSeoSetting } from '@/services'; +import { SchemaForm, JSONSchema, initFormData, UISchema } from '@/components'; +import { useToast } from '@/hooks'; +import { handleFormError } from '@/utils'; + +const Index: FC = () => { + const { t } = useTranslation('translation', { + keyPrefix: 'admin.css_and_html', + }); + const Toast = useToast(); + const schema: JSONSchema = { + title: t('page_title'), + properties: { + custom_css: { + type: 'string', + title: t('custom_css.label'), + description: t('custom_css.text'), + }, + head: { + type: 'string', + title: t('head.label'), + description: t('head.text'), + }, + header: { + type: 'string', + title: t('header.label'), + description: t('header.text'), + }, + footer: { + type: 'string', + title: t('footer.label'), + description: t('footer.text'), + }, + }, + }; + const uiSchema: UISchema = { + custom_css: { + 'ui:widget': 'textarea', + 'ui:options': { + rows: 10, + }, + }, + head: { + 'ui:widget': 'textarea', + 'ui:options': { + rows: 10, + }, + }, + header: { + 'ui:widget': 'textarea', + 'ui:options': { + rows: 10, + }, + }, + footer: { + 'ui:widget': 'textarea', + 'ui:options': { + rows: 10, + }, + }, + }; + const [formData, setFormData] = useState(initFormData(schema)); + + const onSubmit = (evt) => { + evt.preventDefault(); + evt.stopPropagation(); + + const reqParams: Type.AdminSettingsSeo = { + robots: formData.robots.value, + }; + + putSeoSetting(reqParams) + .then(() => { + Toast.onShow({ + msg: t('update', { keyPrefix: 'toast' }), + variant: 'success', + }); + }) + .catch((err) => { + if (err.isError) { + const data = handleFormError(err, formData); + setFormData({ ...data }); + } + }); + }; + + useEffect(() => { + getSeoSetting().then((setting) => { + if (setting) { + const formMeta = { ...formData }; + formMeta.robots.value = setting.robots; + setFormData(formMeta); + } + }); + }, []); + + const handleOnChange = (data) => { + setFormData(data); + }; + + return ( + <> +

{t('page_title')}

+ + + ); +}; + +export default Index; diff --git a/ui/src/pages/Admin/Login/index.tsx b/ui/src/pages/Admin/Login/index.tsx new file mode 100644 index 00000000..5f95d2f7 --- /dev/null +++ b/ui/src/pages/Admin/Login/index.tsx @@ -0,0 +1,93 @@ +import { FC, useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; + +import type * as Type from '@/common/interface'; +import { getSeoSetting, putSeoSetting } from '@/services'; +import { SchemaForm, JSONSchema, initFormData, UISchema } from '@/components'; +import { useToast } from '@/hooks'; +import { handleFormError } from '@/utils'; + +const Index: FC = () => { + const { t } = useTranslation('translation', { + keyPrefix: 'admin.login', + }); + const Toast = useToast(); + const schema: JSONSchema = { + title: t('page_title'), + properties: { + membership: { + type: 'boolean', + title: t('membership.label'), + label: t('membership.labelAlias'), + description: t('membership.text'), + default: true, + }, + private: { + type: 'string', + title: t('private.label'), + description: t('private.text'), + }, + }, + }; + const uiSchema: UISchema = { + membership: { + 'ui:widget': 'switch', + }, + private: { + 'ui:widget': 'switch', + }, + }; + const [formData, setFormData] = useState(initFormData(schema)); + + const onSubmit = (evt) => { + evt.preventDefault(); + evt.stopPropagation(); + + const reqParams: Type.AdminSettingsSeo = { + robots: formData.robots.value, + }; + + putSeoSetting(reqParams) + .then(() => { + Toast.onShow({ + msg: t('update', { keyPrefix: 'toast' }), + variant: 'success', + }); + }) + .catch((err) => { + if (err.isError) { + const data = handleFormError(err, formData); + setFormData({ ...data }); + } + }); + }; + + useEffect(() => { + getSeoSetting().then((setting) => { + if (setting) { + const formMeta = { ...formData }; + formMeta.robots.value = setting.robots; + setFormData(formMeta); + } + }); + }, []); + + const handleOnChange = (data) => { + setFormData(data); + }; + + return ( + <> +

{t('page_title')}

+ + + ); +}; + +export default Index; diff --git a/ui/src/pages/Admin/Themes/index.tsx b/ui/src/pages/Admin/Themes/index.tsx new file mode 100644 index 00000000..58aa7389 --- /dev/null +++ b/ui/src/pages/Admin/Themes/index.tsx @@ -0,0 +1,105 @@ +import { FC, useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; + +import type * as Type from '@/common/interface'; +import { getSeoSetting, putSeoSetting } from '@/services'; +import { SchemaForm, JSONSchema, initFormData, UISchema } from '@/components'; +import { useToast } from '@/hooks'; +import { handleFormError } from '@/utils'; + +const Index: FC = () => { + const { t } = useTranslation('translation', { + keyPrefix: 'admin.themes', + }); + const Toast = useToast(); + const schema: JSONSchema = { + title: t('page_title'), + properties: { + themes: { + type: 'string', + title: t('themes.label'), + description: t('themes.text'), + enum: ['default'], + enumNames: ['Default'], + }, + navbar_style: { + type: 'string', + title: t('navbar_style.label'), + description: t('navbar_style.text'), + enum: ['colored', 'light'], + enumNames: ['Colored', 'Light'], + }, + primary_color: { + type: 'string', + title: t('primary_color.label'), + description: t('primary_color.text'), + }, + }, + }; + const uiSchema: UISchema = { + themes: { + 'ui:widget': 'select', + }, + navbar_style: { + 'ui:widget': 'select', + }, + primary_color: { + 'ui:options': { + type: 'color', + }, + }, + }; + const [formData, setFormData] = useState(initFormData(schema)); + + const onSubmit = (evt) => { + evt.preventDefault(); + evt.stopPropagation(); + + const reqParams: Type.AdminSettingsSeo = { + robots: formData.robots.value, + }; + + putSeoSetting(reqParams) + .then(() => { + Toast.onShow({ + msg: t('update', { keyPrefix: 'toast' }), + variant: 'success', + }); + }) + .catch((err) => { + if (err.isError) { + const data = handleFormError(err, formData); + setFormData({ ...data }); + } + }); + }; + + useEffect(() => { + getSeoSetting().then((setting) => { + if (setting) { + const formMeta = { ...formData }; + formMeta.robots.value = setting.robots; + setFormData(formMeta); + } + }); + }, []); + + const handleOnChange = (data) => { + setFormData(data); + }; + + return ( + <> +

{t('page_title')}

+ + + ); +}; + +export default Index; diff --git a/ui/src/pages/Admin/Users/index.tsx b/ui/src/pages/Admin/Users/index.tsx index b425f49e..915bb2c4 100644 --- a/ui/src/pages/Admin/Users/index.tsx +++ b/ui/src/pages/Admin/Users/index.tsx @@ -1,5 +1,5 @@ import { FC } from 'react'; -import { Form, Table, Dropdown } from 'react-bootstrap'; +import { Form, Table, Dropdown, Button } from 'react-bootstrap'; import { useSearchParams } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; @@ -14,7 +14,13 @@ import { Icon, } from '@/components'; import * as Type from '@/common/interface'; -import { useChangeModal, useChangeUserRoleModal, useToast } from '@/hooks'; +import { + useUserModal, + useChangeModal, + useChangeUserRoleModal, + useChangePasswordModal, + useToast, +} from '@/hooks'; import { useQueryUsers } from '@/services'; import { loggedUserInfoStore } from '@/stores'; import { formatCount } from '@/utils'; @@ -44,6 +50,8 @@ const Users: FC = () => { const curQuery = urlSearchParams.get('query') || ''; const currentUser = loggedUserInfoStore((state) => state.user); const Toast = useToast(); + const userModal = useUserModal(); + const changePasswordModal = useChangePasswordModal(); const { data, isLoading, @@ -99,12 +107,21 @@ const Users: FC = () => { <>

{t('title')}

- +
+ + +
{ + changePasswordModal.onShow()}> + {t('set_new_password')} + handleAction('status', user)}> {t('change_status')} diff --git a/ui/src/router/routes.ts b/ui/src/router/routes.ts index 099c6a38..154dfa9d 100644 --- a/ui/src/router/routes.ts +++ b/ui/src/router/routes.ts @@ -237,6 +237,14 @@ const routes: RouteNode[] = [ path: 'flags', page: 'pages/Admin/Flags', }, + { + path: 'themes', + page: 'pages/Admin/Themes', + }, + { + path: 'css_and_html', + page: 'pages/Admin/CssAndHtml', + }, { path: 'general', page: 'pages/Admin/General', @@ -277,6 +285,10 @@ const routes: RouteNode[] = [ path: 'seo', page: 'pages/Admin/Seo', }, + { + path: 'login', + page: 'pages/Admin/Login', + }, ], }, // for review From a5a95c5f3d1630113b7000cdb922c947b3239091 Mon Sep 17 00:00:00 2001 From: "haitao(lj)" Date: Fri, 9 Dec 2022 15:39:30 +0800 Subject: [PATCH 2/6] refactor(AccordionNav): Optimize the secondary navigation menu --- ui/src/components/AccordionNav/index.css | 7 + ui/src/components/AccordionNav/index.tsx | 163 ++++++++++++----------- ui/src/pages/Admin/Themes/index.tsx | 6 +- 3 files changed, 96 insertions(+), 80 deletions(-) create mode 100644 ui/src/components/AccordionNav/index.css diff --git a/ui/src/components/AccordionNav/index.css b/ui/src/components/AccordionNav/index.css new file mode 100644 index 00000000..c4f89c54 --- /dev/null +++ b/ui/src/components/AccordionNav/index.css @@ -0,0 +1,7 @@ +.collapse-indicator { + transition: all .2s ease; +} + +.expanding .collapse-indicator { + transform: rotate(90deg); +} diff --git a/ui/src/components/AccordionNav/index.tsx b/ui/src/components/AccordionNav/index.tsx index dd0ef6f0..7e9403f5 100644 --- a/ui/src/components/AccordionNav/index.tsx +++ b/ui/src/components/AccordionNav/index.tsx @@ -1,44 +1,63 @@ -import React, { FC } from 'react'; -import { Accordion, Button, Stack } from 'react-bootstrap'; +import React, { FC, useEffect, useState } from 'react'; +import { Accordion, Nav } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; import { useNavigate, useMatch } from 'react-router-dom'; -import { useAccordionButton } from 'react-bootstrap/AccordionButton'; +import classNames from 'classnames'; import { Icon } from '@/components'; +import './index.css'; -function MenuNode({ menu, callback, activeKey, isLeaf = false }) { +function MenuNode({ + menu, + callback, + activeKey, + expanding = false, + path = '/', +}) { const { t } = useTranslation('translation', { keyPrefix: 'nav_menus' }); - const accordionClick = useAccordionButton(menu.name); - const menuOnClick = (evt) => { - evt.preventDefault(); - evt.stopPropagation(); - if (!isLeaf) { - accordionClick(evt); - } - if (typeof callback === 'function') { - callback(menu); - } - }; + const isLeaf = !menu.children.length; + const href = isLeaf ? `${path}${menu.name}` : '#'; - let menuCls = 'text-start text-dark text-nowrap shadow-none bg-body border-0'; - let menuVariant = 'light'; - if (activeKey === menu.name) { - menuCls = 'text-start text-white text-nowrap shadow-none'; - menuVariant = 'primary'; - } return ( - + {!isLeaf && ( + + )} + + {menu.children.length ? ( + + <> + {menu.children.map((leaf) => { + return ( + + ); + })} + + + ) : null} + ); } @@ -46,12 +65,9 @@ interface AccordionProps { menus: any[]; path?: string; } -const AccordionNav: FC = ({ menus, path = '/' }) => { +const AccordionNav: FC = ({ menus = [], path = '/' }) => { const navigate = useNavigate(); const pathMatch = useMatch(`${path}*`); - if (!menus.length) { - return null; - } // auto set menu fields menus.forEach((m) => { if (!Array.isArray(m.children)) { @@ -68,57 +84,50 @@ const AccordionNav: FC = ({ menus, path = '/' }) => { if (splat) { activeKey = splat; } - const menuClick = (clickedMenu) => { - const menuKey = clickedMenu.name; - if (clickedMenu.children.length) { - return; - } - if (activeKey !== menuKey) { - const routePath = `${path}${menuKey}`; - navigate(routePath); - } + const getOpenKey = () => { + let openKey = ''; + menus.forEach((li) => { + if (li.children.length) { + const matchedChild = li.children.find((el) => { + return el.name === activeKey; + }); + if (matchedChild) { + openKey = li.name; + } + } + }); + return openKey; }; - let defaultOpenKey; - menus.forEach((li) => { - if (li.children.length) { - const matchedChild = li.children.find((el) => { - return el.name === activeKey; - }); - if (matchedChild) { - defaultOpenKey = li.name; - } + const [openKey, setOpenKey] = useState(getOpenKey()); + const menuClick = (evt, menu, href, isLeaf) => { + evt.preventDefault(); + evt.stopPropagation(); + if (isLeaf) { + navigate(href); + } else { + setOpenKey(openKey === menu.name ? '' : menu.name); } - }); - + }; + useEffect(() => { + setOpenKey(getOpenKey()); + }, [activeKey]); return ( - - + + ); }; diff --git a/ui/src/pages/Admin/Themes/index.tsx b/ui/src/pages/Admin/Themes/index.tsx index 58aa7389..5514dc14 100644 --- a/ui/src/pages/Admin/Themes/index.tsx +++ b/ui/src/pages/Admin/Themes/index.tsx @@ -77,9 +77,9 @@ const Index: FC = () => { useEffect(() => { getSeoSetting().then((setting) => { if (setting) { - const formMeta = { ...formData }; - formMeta.robots.value = setting.robots; - setFormData(formMeta); + // const formMeta = { ...formData }; + // formMeta.robots.value = setting.robots; + // setFormData(formMeta); } }); }, []); From b062a71234609446f337a1e23a8261df47343190 Mon Sep 17 00:00:00 2001 From: "haitao(lj)" Date: Sat, 10 Dec 2022 14:45:25 +0800 Subject: [PATCH 3/6] feat(admin/theme): Complete theme api interfacing --- i18n/en_US.yaml | 2 +- ui/src/common/constants.ts | 2 +- ui/src/common/interface.ts | 10 +++++ ui/src/components/FollowingTags/index.tsx | 5 ++- ui/src/pages/Admin/Themes/index.tsx | 48 +++++++++++++++++------ ui/src/pages/Admin/index.tsx | 2 + ui/src/router/routes.ts | 2 +- ui/src/services/admin/settings.ts | 10 +++++ 8 files changed, 63 insertions(+), 18 deletions(-) diff --git a/i18n/en_US.yaml b/i18n/en_US.yaml index 1587c02d..b0d5e02c 100644 --- a/i18n/en_US.yaml +++ b/i18n/en_US.yaml @@ -982,7 +982,7 @@ ui: seo: SEO customize: Customize themes: Themes - css_and_html: CSS/HTML + css-html: CSS/HTML login: Login admin: admin_header: diff --git a/ui/src/common/constants.ts b/ui/src/common/constants.ts index 067bb88b..23844d4f 100644 --- a/ui/src/common/constants.ts +++ b/ui/src/common/constants.ts @@ -60,7 +60,7 @@ export const ADMIN_NAV_MENUS = [ name: 'themes', }, { - name: 'css_and_html', + name: 'css-html', }, ], }, diff --git a/ui/src/common/interface.ts b/ui/src/common/interface.ts index c6aa42a5..1144dae6 100644 --- a/ui/src/common/interface.ts +++ b/ui/src/common/interface.ts @@ -346,6 +346,16 @@ export interface AdminSettingsSeo { robots: string; } +export type themeConfig = { + navbar_style: string; + primary_color: string; + [k: string]: string | number; +}; +export interface AdminSettingsTheme { + theme: string; + theme_config: Record; +} + /** * @description interface for Activity */ diff --git a/ui/src/components/FollowingTags/index.tsx b/ui/src/components/FollowingTags/index.tsx index 3b7f64dd..2ae65602 100644 --- a/ui/src/components/FollowingTags/index.tsx +++ b/ui/src/components/FollowingTags/index.tsx @@ -35,7 +35,6 @@ const Index: FC = () => { if (!tryLoggedAndActivated().ok) { return null; } - return isEdit ? ( @@ -80,7 +79,9 @@ const Index: FC = () => { <>
{t('follow_tag_tip')}
- + )} diff --git a/ui/src/pages/Admin/Themes/index.tsx b/ui/src/pages/Admin/Themes/index.tsx index 5514dc14..cbec8607 100644 --- a/ui/src/pages/Admin/Themes/index.tsx +++ b/ui/src/pages/Admin/Themes/index.tsx @@ -2,7 +2,7 @@ import { FC, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import type * as Type from '@/common/interface'; -import { getSeoSetting, putSeoSetting } from '@/services'; +import { getThemeSetting, putThemeSetting } from '@/services'; import { SchemaForm, JSONSchema, initFormData, UISchema } from '@/components'; import { useToast } from '@/hooks'; import { handleFormError } from '@/utils'; @@ -21,6 +21,7 @@ const Index: FC = () => { description: t('themes.text'), enum: ['default'], enumNames: ['Default'], + default: 'default', }, navbar_style: { type: 'string', @@ -33,6 +34,7 @@ const Index: FC = () => { type: 'string', title: t('primary_color.label'), description: t('primary_color.text'), + default: '#ffffff', }, }, }; @@ -49,17 +51,22 @@ const Index: FC = () => { }, }, }; + const [themeSetting, setThemeSetting] = useState(); const [formData, setFormData] = useState(initFormData(schema)); - const onSubmit = (evt) => { evt.preventDefault(); evt.stopPropagation(); - - const reqParams: Type.AdminSettingsSeo = { - robots: formData.robots.value, + const themeName = formData.themes.value; + const reqParams: Type.AdminSettingsTheme = { + theme: themeName, + theme_config: { + [themeName]: { + navbar_style: formData.navbar_style.value, + primary_color: formData.primary_color.value, + }, + }, }; - - putSeoSetting(reqParams) + putThemeSetting(reqParams) .then(() => { Toast.onShow({ msg: t('update', { keyPrefix: 'toast' }), @@ -75,17 +82,32 @@ const Index: FC = () => { }; useEffect(() => { - getSeoSetting().then((setting) => { + getThemeSetting().then((setting) => { if (setting) { - // const formMeta = { ...formData }; - // formMeta.robots.value = setting.robots; - // setFormData(formMeta); + setThemeSetting(setting); + const themeName = setting.theme; + const themeConfig = setting.theme_config[themeName]; + const formMeta = { ...formData }; + formMeta.themes.value = themeName; + formMeta.navbar_style.value = themeConfig?.navbar_style; + formMeta.primary_color.value = themeConfig?.primary_color; + setFormData({ ...formMeta }); } }); }, []); - const handleOnChange = (data) => { - setFormData(data); + const handleOnChange = (cd) => { + console.log('cd: ', cd); + setFormData(cd); + const themeConfig = themeSetting?.theme_config[cd.themes.value]; + if (themeConfig) { + themeConfig.navbar_style = cd.navbar_style.value; + themeConfig.primary_color = cd.primary_color.value; + setThemeSetting({ + theme: themeSetting?.theme, + theme_config: themeSetting?.theme_config, + }); + } }; return ( diff --git a/ui/src/pages/Admin/index.tsx b/ui/src/pages/Admin/index.tsx index 5841235f..7bc1cb50 100644 --- a/ui/src/pages/Admin/index.tsx +++ b/ui/src/pages/Admin/index.tsx @@ -16,6 +16,8 @@ const formPaths = [ 'branding', 'legal', 'write', + 'themes', + 'css-html', ]; const Index: FC = () => { diff --git a/ui/src/router/routes.ts b/ui/src/router/routes.ts index 154dfa9d..e09071ff 100644 --- a/ui/src/router/routes.ts +++ b/ui/src/router/routes.ts @@ -242,7 +242,7 @@ const routes: RouteNode[] = [ page: 'pages/Admin/Themes', }, { - path: 'css_and_html', + path: 'css-html', page: 'pages/Admin/CssAndHtml', }, { diff --git a/ui/src/services/admin/settings.ts b/ui/src/services/admin/settings.ts index d4b366bb..191b6700 100644 --- a/ui/src/services/admin/settings.ts +++ b/ui/src/services/admin/settings.ts @@ -109,3 +109,13 @@ export const getSeoSetting = () => { export const putSeoSetting = (params: Type.AdminSettingsSeo) => { return request.put('/answer/admin/api/siteinfo/seo', params); }; + +export const getThemeSetting = () => { + return request.get( + '/answer/admin/api/siteinfo/theme', + ); +}; + +export const putThemeSetting = (params: Type.AdminSettingsTheme) => { + return request.put('/answer/admin/api/siteinfo/theme', params); +}; From 806e3c390d244103bb53dc3a3a653243ebdc250e Mon Sep 17 00:00:00 2001 From: "haitao(lj)" Date: Sat, 10 Dec 2022 15:06:46 +0800 Subject: [PATCH 4/6] feat(admin/custom): Complete custom css/html api interfacing --- ui/src/common/interface.ts | 7 ++++++ ui/src/pages/Admin/CssAndHtml/index.tsx | 30 +++++++++++++++---------- ui/src/services/admin/settings.ts | 10 +++++++++ 3 files changed, 35 insertions(+), 12 deletions(-) diff --git a/ui/src/common/interface.ts b/ui/src/common/interface.ts index 1144dae6..ef210f9e 100644 --- a/ui/src/common/interface.ts +++ b/ui/src/common/interface.ts @@ -356,6 +356,13 @@ export interface AdminSettingsTheme { theme_config: Record; } +export interface AdminSettingsCustom { + custom_css: string; + custom_head: string; + custom_header: string; + custom_footer: string; +} + /** * @description interface for Activity */ diff --git a/ui/src/pages/Admin/CssAndHtml/index.tsx b/ui/src/pages/Admin/CssAndHtml/index.tsx index a06b8060..b050d87e 100644 --- a/ui/src/pages/Admin/CssAndHtml/index.tsx +++ b/ui/src/pages/Admin/CssAndHtml/index.tsx @@ -2,7 +2,7 @@ import { FC, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import type * as Type from '@/common/interface'; -import { getSeoSetting, putSeoSetting } from '@/services'; +import { getPageCustom, putPageCustom } from '@/services'; import { SchemaForm, JSONSchema, initFormData, UISchema } from '@/components'; import { useToast } from '@/hooks'; import { handleFormError } from '@/utils'; @@ -20,17 +20,17 @@ const Index: FC = () => { title: t('custom_css.label'), description: t('custom_css.text'), }, - head: { + custom_head: { type: 'string', title: t('head.label'), description: t('head.text'), }, - header: { + custom_header: { type: 'string', title: t('header.label'), description: t('header.text'), }, - footer: { + custom_footer: { type: 'string', title: t('footer.label'), description: t('footer.text'), @@ -44,19 +44,19 @@ const Index: FC = () => { rows: 10, }, }, - head: { + custom_head: { 'ui:widget': 'textarea', 'ui:options': { rows: 10, }, }, - header: { + custom_header: { 'ui:widget': 'textarea', 'ui:options': { rows: 10, }, }, - footer: { + custom_footer: { 'ui:widget': 'textarea', 'ui:options': { rows: 10, @@ -69,11 +69,14 @@ const Index: FC = () => { evt.preventDefault(); evt.stopPropagation(); - const reqParams: Type.AdminSettingsSeo = { - robots: formData.robots.value, + const reqParams: Type.AdminSettingsCustom = { + custom_css: formData.custom_css.value, + custom_head: formData.custom_head.value, + custom_header: formData.custom_header.value, + custom_footer: formData.custom_footer.value, }; - putSeoSetting(reqParams) + putPageCustom(reqParams) .then(() => { Toast.onShow({ msg: t('update', { keyPrefix: 'toast' }), @@ -89,10 +92,13 @@ const Index: FC = () => { }; useEffect(() => { - getSeoSetting().then((setting) => { + getPageCustom().then((setting) => { if (setting) { const formMeta = { ...formData }; - formMeta.robots.value = setting.robots; + formMeta.custom_css.value = setting.custom_css; + formMeta.custom_head.value = setting.custom_head; + formMeta.custom_header.value = setting.custom_header; + formMeta.custom_footer.value = setting.custom_footer; setFormData(formMeta); } }); diff --git a/ui/src/services/admin/settings.ts b/ui/src/services/admin/settings.ts index 191b6700..4cbe009d 100644 --- a/ui/src/services/admin/settings.ts +++ b/ui/src/services/admin/settings.ts @@ -119,3 +119,13 @@ export const getThemeSetting = () => { export const putThemeSetting = (params: Type.AdminSettingsTheme) => { return request.put('/answer/admin/api/siteinfo/theme', params); }; + +export const getPageCustom = () => { + return request.get( + '/answer/admin/api/siteinfo/custom-css-html', + ); +}; + +export const putPageCustom = (params: Type.AdminSettingsCustom) => { + return request.put('/answer/admin/api/siteinfo/custom-css-html', params); +}; From 481d16ba16cb70c70eb9174c74522f18e8488cf1 Mon Sep 17 00:00:00 2001 From: "haitao(lj)" Date: Sat, 10 Dec 2022 15:30:36 +0800 Subject: [PATCH 5/6] feat(admin/login): Complete login setting api interfacing --- i18n/en_US.yaml | 11 +++++----- ui/src/common/interface.ts | 5 +++++ ui/src/pages/Admin/Login/index.tsx | 35 +++++++++++++++++------------- ui/src/services/admin/settings.ts | 10 +++++++++ 4 files changed, 41 insertions(+), 20 deletions(-) diff --git a/i18n/en_US.yaml b/i18n/en_US.yaml index b0d5e02c..7581aca7 100644 --- a/i18n/en_US.yaml +++ b/i18n/en_US.yaml @@ -1096,7 +1096,7 @@ ui: label: Password btn_cancel: Cancel btn_submit: Submit - user_modal: + user_modal: title: Add new user form: fields: @@ -1109,7 +1109,7 @@ ui: label: Password btn_cancel: Cancel btn_submit: Submit - + questions: page_title: Questions normal: Normal @@ -1290,11 +1290,12 @@ ui: login: page_title: Login membership: - label: Membership - labelAlias: Allow new registrations + title: Membership + label: Allow new registrations text: Turn off to prevent anyone from creating a new account. private: - label: Private + title: Private + label: Login required text: Only logged in users can access this community. form: diff --git a/ui/src/common/interface.ts b/ui/src/common/interface.ts index ef210f9e..f657272d 100644 --- a/ui/src/common/interface.ts +++ b/ui/src/common/interface.ts @@ -363,6 +363,11 @@ export interface AdminSettingsCustom { custom_footer: string; } +export interface AdminSettingsLogin { + allow_new_registrations: boolean; + login_required: boolean; +} + /** * @description interface for Activity */ diff --git a/ui/src/pages/Admin/Login/index.tsx b/ui/src/pages/Admin/Login/index.tsx index 5f95d2f7..94c3cb67 100644 --- a/ui/src/pages/Admin/Login/index.tsx +++ b/ui/src/pages/Admin/Login/index.tsx @@ -2,7 +2,7 @@ import { FC, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import type * as Type from '@/common/interface'; -import { getSeoSetting, putSeoSetting } from '@/services'; +import { getLoginSetting, putLoginSetting } from '@/services'; import { SchemaForm, JSONSchema, initFormData, UISchema } from '@/components'; import { useToast } from '@/hooks'; import { handleFormError } from '@/utils'; @@ -15,25 +15,27 @@ const Index: FC = () => { const schema: JSONSchema = { title: t('page_title'), properties: { - membership: { + allow_new_registrations: { type: 'boolean', - title: t('membership.label'), - label: t('membership.labelAlias'), + title: t('membership.title'), + label: t('membership.label'), description: t('membership.text'), default: true, }, - private: { - type: 'string', - title: t('private.label'), + login_required: { + type: 'boolean', + title: t('private.title'), + label: t('private.label'), description: t('private.text'), + default: false, }, }, }; const uiSchema: UISchema = { - membership: { + allow_new_registrations: { 'ui:widget': 'switch', }, - private: { + login_required: { 'ui:widget': 'switch', }, }; @@ -43,11 +45,12 @@ const Index: FC = () => { evt.preventDefault(); evt.stopPropagation(); - const reqParams: Type.AdminSettingsSeo = { - robots: formData.robots.value, + const reqParams: Type.AdminSettingsLogin = { + allow_new_registrations: formData.allow_new_registrations.value, + login_required: formData.login_required.value, }; - putSeoSetting(reqParams) + putLoginSetting(reqParams) .then(() => { Toast.onShow({ msg: t('update', { keyPrefix: 'toast' }), @@ -63,11 +66,13 @@ const Index: FC = () => { }; useEffect(() => { - getSeoSetting().then((setting) => { + getLoginSetting().then((setting) => { if (setting) { const formMeta = { ...formData }; - formMeta.robots.value = setting.robots; - setFormData(formMeta); + formMeta.allow_new_registrations.value = + setting.allow_new_registrations; + formMeta.login_required.value = setting.login_required; + setFormData({ ...formMeta }); } }); }, []); diff --git a/ui/src/services/admin/settings.ts b/ui/src/services/admin/settings.ts index 4cbe009d..e458e472 100644 --- a/ui/src/services/admin/settings.ts +++ b/ui/src/services/admin/settings.ts @@ -129,3 +129,13 @@ export const getPageCustom = () => { export const putPageCustom = (params: Type.AdminSettingsCustom) => { return request.put('/answer/admin/api/siteinfo/custom-css-html', params); }; + +export const getLoginSetting = () => { + return request.get( + '/answer/admin/api/siteinfo/login', + ); +}; + +export const putLoginSetting = (params: Type.AdminSettingsLogin) => { + return request.put('/answer/admin/api/siteinfo/login', params); +}; From 79f25466ca25b4b411bad76d5e5a5922167fa9b3 Mon Sep 17 00:00:00 2001 From: "haitao(lj)" Date: Sat, 10 Dec 2022 16:37:18 +0800 Subject: [PATCH 6/6] feat(admin/users): add new user and change user password done --- i18n/en_US.yaml | 2 +- ui/src/hooks/useChangePasswordModal/index.tsx | 26 ++++------ ui/src/hooks/useUserModal/index.tsx | 12 ++--- ui/src/pages/Admin/Users/index.tsx | 50 +++++++++++++------ ui/src/services/admin/users.ts | 15 ++++++ 5 files changed, 66 insertions(+), 39 deletions(-) diff --git a/i18n/en_US.yaml b/i18n/en_US.yaml index 7581aca7..2f9efa80 100644 --- a/i18n/en_US.yaml +++ b/i18n/en_US.yaml @@ -705,7 +705,7 @@ ui: update: update success update_password: Password changed successfully. flag_success: Thanks for flagging. - fobidden_operate_self: Forbidden to operate on yourself + forbidden_operate_self: Forbidden to operate on yourself review: Your revision will show after review. related_question: title: Related Questions diff --git a/ui/src/hooks/useChangePasswordModal/index.tsx b/ui/src/hooks/useChangePasswordModal/index.tsx index ba0b7325..a8c046c1 100644 --- a/ui/src/hooks/useChangePasswordModal/index.tsx +++ b/ui/src/hooks/useChangePasswordModal/index.tsx @@ -21,18 +21,19 @@ const useChangePasswordModal = (props: IProps = {}) => { const { title = t('title'), onConfirm } = props; const [visible, setVisibleState] = useState(false); + const [userId, setUserId] = useState(''); const schema: JSONSchema = { title: t('title'), required: ['password'], properties: { - new_password: { + password: { type: 'string', title: t('form.fields.password.label'), }, }, }; const uiSchema: UISchema = { - new_password: { + password: { 'ui:options': { type: 'password', }, @@ -50,7 +51,8 @@ const useChangePasswordModal = (props: IProps = {}) => { setVisibleState(false); }; - const onShow = () => { + const onShow = (user_id: string) => { + setUserId(user_id); setVisibleState(true); }; @@ -65,27 +67,17 @@ const useChangePasswordModal = (props: IProps = {}) => { if (onConfirm instanceof Function) { onConfirm({ - slug_name: formData.slugName.value, - display_name: formData.displayName.value, - original_text: formData.description.value, + password: formData.password.value, + user_id: userId, }); setFormData({ - displayName: { - value: '', - isInvalid: false, - errorMsg: '', - }, - slugName: { - value: '', - isInvalid: false, - errorMsg: '', - }, - description: { + password: { value: '', isInvalid: false, errorMsg: '', }, }); + setUserId(''); } onClose(); }; diff --git a/ui/src/hooks/useUserModal/index.tsx b/ui/src/hooks/useUserModal/index.tsx index 713a2caa..4ce41f8c 100644 --- a/ui/src/hooks/useUserModal/index.tsx +++ b/ui/src/hooks/useUserModal/index.tsx @@ -85,22 +85,22 @@ const useAddUserModal = (props: IProps = {}) => { if (onConfirm instanceof Function) { onConfirm({ - slug_name: formData.slugName.value, - display_name: formData.displayName.value, - original_text: formData.description.value, + display_name: formData.display_name.value, + email: formData.email.value, + password: formData.password.value, }); setFormData({ - displayName: { + display_name: { value: '', isInvalid: false, errorMsg: '', }, - slugName: { + email: { value: '', isInvalid: false, errorMsg: '', }, - description: { + password: { value: '', isInvalid: false, errorMsg: '', diff --git a/ui/src/pages/Admin/Users/index.tsx b/ui/src/pages/Admin/Users/index.tsx index 11977b19..b6dbabe7 100644 --- a/ui/src/pages/Admin/Users/index.tsx +++ b/ui/src/pages/Admin/Users/index.tsx @@ -1,5 +1,5 @@ import { FC } from 'react'; -import { Form, Table, Dropdown, Button } from 'react-bootstrap'; +import { Form, Table, Dropdown, Button, Stack } from 'react-bootstrap'; import { useSearchParams } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; @@ -21,7 +21,7 @@ import { useChangePasswordModal, useToast, } from '@/hooks'; -import { useQueryUsers } from '@/services'; +import { useQueryUsers, addUser, updateUserPassword } from '@/services'; import { loggedUserInfoStore } from '@/stores'; import { formatCount } from '@/utils'; @@ -50,8 +50,6 @@ const Users: FC = () => { const curQuery = urlSearchParams.get('query') || ''; const currentUser = loggedUserInfoStore((state) => state.user); const Toast = useToast(); - const userModal = useUserModal(); - const changePasswordModal = useChangePasswordModal(); const { data, isLoading, @@ -74,11 +72,31 @@ const Users: FC = () => { callback: refreshUsers, }); + const userModal = useUserModal({ + onConfirm: (userModel) => { + addUser(userModel).then(() => { + if (/all|staff/.test(curFilter) && curPage === 1) { + refreshUsers(); + } + }); + }, + }); + const changePasswordModal = useChangePasswordModal({ + onConfirm: (rd) => { + updateUserPassword(rd).then(() => { + Toast.onShow({ + msg: t('update_password', { keyPrefix: 'toast' }), + variant: 'success', + }); + }); + }, + }); + const handleAction = (type, user) => { const { user_id, status, role_id, username } = user; if (username === currentUser.username) { Toast.onShow({ - msg: t('fobidden_operate_self', { keyPrefix: 'toast' }), + msg: t('forbidden_operate_self', { keyPrefix: 'toast' }), variant: 'warning', }); return; @@ -96,6 +114,9 @@ const Users: FC = () => { role_id, }); } + if (type === 'password') { + changePasswordModal.onShow(user_id); + } }; const handleFilter = (e) => { @@ -107,21 +128,20 @@ const Users: FC = () => { <>

{t('title')}

-
- + -
+ + { changePasswordModal.onShow()}> + onClick={() => handleAction('password', user)}> {t('set_new_password')} { export const changeUserRole = (params) => { return request.put('/answer/admin/api/user/role', params); }; + +export const addUser = (params: { + display_name: string; + email: string; + password: string; +}) => { + return request.post('/answer/admin/api/user', params); +}; + +export const updateUserPassword = (params: { + password: string; + user_id: string; +}) => { + return request.put('/answer/admin/api/user/password', params); +};