mirror of
https://github.com/usememos/memos.git
synced 2025-01-03 03:38:23 +03:00
feat: member manage section in setting dialog
This commit is contained in:
parent
fbf4afff8e
commit
c492317ffe
@ -27,9 +27,10 @@ type User struct {
|
||||
|
||||
type UserCreate struct {
|
||||
// Domain specific fields
|
||||
Email string
|
||||
Role Role
|
||||
Name string
|
||||
Email string `json:"email"`
|
||||
Role Role `json:"role"`
|
||||
Name string `json:"name"`
|
||||
Password string `json:"password"`
|
||||
PasswordHash string
|
||||
OpenID string
|
||||
}
|
||||
|
@ -81,24 +81,10 @@ func (s *Server) registerAuthRoutes(g *echo.Group) {
|
||||
if len(signup.Email) < 6 {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "Email is too short, minimum length is 6.")
|
||||
}
|
||||
if len(signup.Name) < 6 {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "Username is too short, minimum length is 6.")
|
||||
}
|
||||
if len(signup.Password) < 6 {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "Password is too short, minimum length is 6.")
|
||||
}
|
||||
|
||||
userFind := &api.UserFind{
|
||||
Email: &signup.Email,
|
||||
}
|
||||
user, err := s.Store.FindUser(userFind)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to find user by email %s", signup.Email)).SetInternal(err)
|
||||
}
|
||||
if user != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Existed user found: %s", signup.Email))
|
||||
}
|
||||
|
||||
passwordHash, err := bcrypt.GenerateFromPassword([]byte(signup.Password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to generate password hash").SetInternal(err)
|
||||
@ -111,7 +97,7 @@ func (s *Server) registerAuthRoutes(g *echo.Group) {
|
||||
PasswordHash: string(passwordHash),
|
||||
OpenID: common.GenUUID(),
|
||||
}
|
||||
user, err = s.Store.CreateUser(userCreate)
|
||||
user, err := s.Store.CreateUser(userCreate)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create user").SetInternal(err)
|
||||
}
|
||||
|
@ -12,6 +12,45 @@ import (
|
||||
)
|
||||
|
||||
func (s *Server) registerUserRoutes(g *echo.Group) {
|
||||
g.POST("/user", func(c echo.Context) error {
|
||||
userCreate := &api.UserCreate{}
|
||||
if err := json.NewDecoder(c.Request().Body).Decode(userCreate); err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post user request").SetInternal(err)
|
||||
}
|
||||
|
||||
passwordHash, err := bcrypt.GenerateFromPassword([]byte(userCreate.Password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to generate password hash").SetInternal(err)
|
||||
}
|
||||
|
||||
userCreate.PasswordHash = string(passwordHash)
|
||||
user, err := s.Store.CreateUser(userCreate)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create user").SetInternal(err)
|
||||
}
|
||||
|
||||
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
|
||||
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(user)); err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode user response").SetInternal(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
g.GET("/user", func(c echo.Context) error {
|
||||
userList, err := s.Store.FindUserList(&api.UserFind{})
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch user list").SetInternal(err)
|
||||
}
|
||||
|
||||
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
|
||||
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(userList)); err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode user list response").SetInternal(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
// GET /api/user/me is used to check if the user is logged in.
|
||||
g.GET("/user/me", func(c echo.Context) error {
|
||||
userSessionID := c.Get(getUserIDContextKey())
|
||||
|
@ -1,14 +1,13 @@
|
||||
-- user
|
||||
CREATE TABLE user (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
email TEXT NOT NULL,
|
||||
email TEXT NOT NULL UNIQUE,
|
||||
role TEXT NOT NULL CHECK (role IN ('OWNER', 'USER')) DEFAULT 'USER',
|
||||
name TEXT NOT NULL,
|
||||
password_hash TEXT NOT NULL,
|
||||
open_id TEXT NOT NULL,
|
||||
open_id TEXT NOT NULL UNIQUE,
|
||||
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
|
||||
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
|
||||
UNIQUE(`email`, `open_id`)
|
||||
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now'))
|
||||
);
|
||||
|
||||
INSERT INTO
|
||||
|
@ -25,6 +25,15 @@ func (s *Store) PatchUser(patch *api.UserPatch) (*api.User, error) {
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (s *Store) FindUserList(find *api.UserFind) ([]*api.User, error) {
|
||||
list, err := findUserList(s.db, find)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return list, nil
|
||||
}
|
||||
|
||||
func (s *Store) FindUser(find *api.UserFind) (*api.User, error) {
|
||||
list, err := findUserList(s.db, find)
|
||||
if err != nil {
|
||||
|
@ -246,7 +246,7 @@ const MemoEditor: React.FC<Props> = () => {
|
||||
const file = inputEl.files[0];
|
||||
const url = await handleUploadFile(file);
|
||||
if (url) {
|
||||
editorRef.current?.insertText(url);
|
||||
editorRef.current?.insertText(url + " ");
|
||||
}
|
||||
};
|
||||
inputEl.click();
|
||||
@ -259,7 +259,7 @@ const MemoEditor: React.FC<Props> = () => {
|
||||
}
|
||||
}, []);
|
||||
|
||||
const showEditStatus = Boolean(globalState.editMemoId);
|
||||
const isEditing = Boolean(globalState.editMemoId);
|
||||
|
||||
const editorConfig = useMemo(
|
||||
() => ({
|
||||
@ -267,17 +267,17 @@ const MemoEditor: React.FC<Props> = () => {
|
||||
initialContent: getEditorContentCache(),
|
||||
placeholder: "Any thoughts...",
|
||||
showConfirmBtn: true,
|
||||
showCancelBtn: showEditStatus,
|
||||
showCancelBtn: isEditing,
|
||||
onConfirmBtnClick: handleSaveBtnClick,
|
||||
onCancelBtnClick: handleCancelBtnClick,
|
||||
onContentChange: handleContentChange,
|
||||
}),
|
||||
[showEditStatus]
|
||||
[isEditing]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={"memo-editor-container " + (showEditStatus ? "edit-ing" : "")}>
|
||||
<p className={"tip-text " + (showEditStatus ? "" : "hidden")}>Editting...</p>
|
||||
<div className={"memo-editor-container " + (isEditing ? "edit-ing" : "")}>
|
||||
<p className={"tip-text " + (isEditing ? "" : "hidden")}>Editting...</p>
|
||||
<Editor
|
||||
ref={editorRef}
|
||||
{...editorConfig}
|
||||
|
@ -1,125 +0,0 @@
|
||||
import { useContext } from "react";
|
||||
import appContext from "../stores/appContext";
|
||||
import { globalStateService, memoService } from "../services";
|
||||
import utils from "../helpers/utils";
|
||||
import { formatMemoContent } from "./Memo";
|
||||
import toastHelper from "./Toast";
|
||||
import "../less/preferences-section.less";
|
||||
|
||||
interface Props {}
|
||||
|
||||
const PreferencesSection: React.FC<Props> = () => {
|
||||
const { globalState } = useContext(appContext);
|
||||
const { shouldHideImageUrl, shouldSplitMemoWord, shouldUseMarkdownParser } = globalState;
|
||||
|
||||
const demoMemoContent = "👋 Hiya, welcome to memos!\n* ✨ **Open source project**;\n* 😋 What do you think;\n* 📑 Tell me something plz;";
|
||||
|
||||
const handleSplitWordsValueChanged = () => {
|
||||
globalStateService.setAppSetting({
|
||||
shouldSplitMemoWord: !shouldSplitMemoWord,
|
||||
});
|
||||
};
|
||||
|
||||
const handleHideImageUrlValueChanged = () => {
|
||||
globalStateService.setAppSetting({
|
||||
shouldHideImageUrl: !shouldHideImageUrl,
|
||||
});
|
||||
};
|
||||
|
||||
const handleUseMarkdownParserChanged = () => {
|
||||
globalStateService.setAppSetting({
|
||||
shouldUseMarkdownParser: !shouldUseMarkdownParser,
|
||||
});
|
||||
};
|
||||
|
||||
const handleExportBtnClick = async () => {
|
||||
const formatedMemos = memoService.getState().memos.map((m) => {
|
||||
return {
|
||||
content: m.content,
|
||||
createdAt: m.createdAt,
|
||||
};
|
||||
});
|
||||
|
||||
const jsonStr = JSON.stringify(formatedMemos);
|
||||
const element = document.createElement("a");
|
||||
element.setAttribute("href", "data:text/json;charset=utf-8," + encodeURIComponent(jsonStr));
|
||||
element.setAttribute("download", `memos-${utils.getDateTimeString(Date.now())}.json`);
|
||||
element.style.display = "none";
|
||||
document.body.appendChild(element);
|
||||
element.click();
|
||||
document.body.removeChild(element);
|
||||
};
|
||||
|
||||
const handleImportBtnClick = async () => {
|
||||
const fileInputEl = document.createElement("input");
|
||||
fileInputEl.type = "file";
|
||||
fileInputEl.accept = "application/JSON";
|
||||
fileInputEl.onchange = () => {
|
||||
if (fileInputEl.files?.length && fileInputEl.files.length > 0) {
|
||||
const reader = new FileReader();
|
||||
reader.readAsText(fileInputEl.files[0]);
|
||||
reader.onload = async (event) => {
|
||||
const memoList = JSON.parse(event.target?.result as string) as Model.Memo[];
|
||||
if (!Array.isArray(memoList)) {
|
||||
toastHelper.error("Unexpected data type.");
|
||||
}
|
||||
|
||||
let succeedAmount = 0;
|
||||
|
||||
for (const memo of memoList) {
|
||||
const content = memo.content || "";
|
||||
const createdAt = memo.createdAt || utils.getDateTimeString(Date.now());
|
||||
|
||||
try {
|
||||
await memoService.importMemo(content, createdAt);
|
||||
succeedAmount++;
|
||||
} catch (error) {
|
||||
// do nth
|
||||
}
|
||||
}
|
||||
|
||||
await memoService.fetchAllMemos();
|
||||
toastHelper.success(`${succeedAmount} memos successfully imported.`);
|
||||
};
|
||||
}
|
||||
};
|
||||
fileInputEl.click();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="section-container preferences-section-container">
|
||||
<p className="title-text">Memo Display</p>
|
||||
<div
|
||||
className="demo-content-container memo-content-text"
|
||||
dangerouslySetInnerHTML={{ __html: formatMemoContent(demoMemoContent) }}
|
||||
></div>
|
||||
<label className="form-label checkbox-form-label hidden" onClick={handleSplitWordsValueChanged}>
|
||||
<span className="normal-text">Auto-space in English and Chinese</span>
|
||||
<img className="icon-img" src={shouldSplitMemoWord ? "/icons/checkbox-active.svg" : "/icons/checkbox.svg"} />
|
||||
</label>
|
||||
<label className="form-label checkbox-form-label" onClick={handleUseMarkdownParserChanged}>
|
||||
<span className="normal-text">Partial markdown format parsing</span>
|
||||
<img className="icon-img" src={shouldUseMarkdownParser ? "/icons/checkbox-active.svg" : "/icons/checkbox.svg"} />
|
||||
</label>
|
||||
<label className="form-label checkbox-form-label" onClick={handleHideImageUrlValueChanged}>
|
||||
<span className="normal-text">Hide image url</span>
|
||||
<img className="icon-img" src={shouldHideImageUrl ? "/icons/checkbox-active.svg" : "/icons/checkbox.svg"} />
|
||||
</label>
|
||||
</div>
|
||||
<div className="section-container">
|
||||
<p className="title-text">Others</p>
|
||||
<div className="w-full flex flex-row justify-start items-center">
|
||||
<button className="px-2 py-1 border rounded text-base hover:opacity-80" onClick={handleExportBtnClick}>
|
||||
Export data as JSON
|
||||
</button>
|
||||
<button className="ml-2 px-2 py-1 border rounded text-base hover:opacity-80" onClick={handleImportBtnClick}>
|
||||
Import from JSON
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default PreferencesSection;
|
@ -1,18 +1,23 @@
|
||||
import { useState } from "react";
|
||||
import { useContext, useState } from "react";
|
||||
import appContext from "../stores/appContext";
|
||||
import { showDialog } from "./Dialog";
|
||||
import MyAccountSection from "./MyAccountSection";
|
||||
import PreferencesSection from "./PreferencesSection";
|
||||
import MyAccountSection from "./Settings/MyAccountSection";
|
||||
import PreferencesSection from "./Settings/PreferencesSection";
|
||||
import MemberSection from "./Settings/MemberSection";
|
||||
import "../less/setting-dialog.less";
|
||||
|
||||
interface Props extends DialogProps {}
|
||||
|
||||
type SettingSection = "my-account" | "preferences";
|
||||
type SettingSection = "my-account" | "preferences" | "member";
|
||||
|
||||
interface State {
|
||||
selectedSection: SettingSection;
|
||||
}
|
||||
|
||||
const SettingDialog: React.FC<Props> = (props: Props) => {
|
||||
const {
|
||||
userState: { user },
|
||||
} = useContext(appContext);
|
||||
const { destroy } = props;
|
||||
const [state, setState] = useState<State>({
|
||||
selectedSection: "my-account",
|
||||
@ -30,6 +35,7 @@ const SettingDialog: React.FC<Props> = (props: Props) => {
|
||||
<img className="icon-img" src="/icons/close.svg" />
|
||||
</button>
|
||||
<div className="section-selector-container">
|
||||
<span className="section-title">Basic</span>
|
||||
<span
|
||||
onClick={() => handleSectionSelectorItemClick("my-account")}
|
||||
className={`section-item ${state.selectedSection === "my-account" ? "selected" : ""}`}
|
||||
@ -42,12 +48,25 @@ const SettingDialog: React.FC<Props> = (props: Props) => {
|
||||
>
|
||||
Preferences
|
||||
</span>
|
||||
{user?.role === "OWNER" ? (
|
||||
<>
|
||||
<span className="section-title">Admin</span>
|
||||
<span
|
||||
onClick={() => handleSectionSelectorItemClick("member")}
|
||||
className={`section-item ${state.selectedSection === "member" ? "selected" : ""}`}
|
||||
>
|
||||
Member
|
||||
</span>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="section-content-container">
|
||||
{state.selectedSection === "my-account" ? (
|
||||
<MyAccountSection />
|
||||
) : state.selectedSection === "preferences" ? (
|
||||
<PreferencesSection />
|
||||
) : state.selectedSection === "member" ? (
|
||||
<MemberSection />
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
96
web/src/components/Settings/MemberSection.tsx
Normal file
96
web/src/components/Settings/MemberSection.tsx
Normal file
@ -0,0 +1,96 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { isEmpty } from "lodash-es";
|
||||
import api from "../../helpers/api";
|
||||
import toastHelper from "../Toast";
|
||||
import "../../less/settings/member-section.less";
|
||||
|
||||
interface Props {}
|
||||
|
||||
interface State {
|
||||
createUserEmail: string;
|
||||
createUserPassword: string;
|
||||
}
|
||||
|
||||
const PreferencesSection: React.FC<Props> = () => {
|
||||
const [state, setState] = useState<State>({
|
||||
createUserEmail: "",
|
||||
createUserPassword: "",
|
||||
});
|
||||
const [userList, setUserList] = useState<Model.User[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchUserList();
|
||||
}, []);
|
||||
|
||||
const fetchUserList = async () => {
|
||||
const data = await api.getUserList();
|
||||
setUserList(data);
|
||||
};
|
||||
|
||||
const handleEmailInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setState({
|
||||
...state,
|
||||
createUserEmail: event.target.value,
|
||||
});
|
||||
};
|
||||
|
||||
const handlePasswordInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setState({
|
||||
...state,
|
||||
createUserPassword: event.target.value,
|
||||
});
|
||||
};
|
||||
|
||||
const handleCreateUserBtnClick = async () => {
|
||||
if (isEmpty(state.createUserEmail) || isEmpty(state.createUserPassword)) {
|
||||
toastHelper.error("Please fill out this form");
|
||||
return;
|
||||
}
|
||||
|
||||
const userCreate: API.UserCreate = {
|
||||
email: state.createUserEmail,
|
||||
password: state.createUserPassword,
|
||||
role: "USER",
|
||||
name: state.createUserEmail,
|
||||
};
|
||||
|
||||
try {
|
||||
await api.createUser(userCreate);
|
||||
} catch (error: any) {
|
||||
toastHelper.error(error.message);
|
||||
}
|
||||
await fetchUserList();
|
||||
setState({
|
||||
createUserEmail: "",
|
||||
createUserPassword: "",
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="section-container member-section-container">
|
||||
<p className="title-text">Create a member</p>
|
||||
<div className="create-member-container">
|
||||
<div className="input-form-container">
|
||||
<span className="field-text">Email</span>
|
||||
<input type="email" placeholder="Email" value={state.createUserEmail} onChange={handleEmailInputChange} />
|
||||
</div>
|
||||
<div className="input-form-container">
|
||||
<span className="field-text">Password</span>
|
||||
<input type="text" placeholder="Password" value={state.createUserPassword} onChange={handlePasswordInputChange} />
|
||||
</div>
|
||||
<div className="btns-container">
|
||||
<button onClick={handleCreateUserBtnClick}>Create</button>
|
||||
</div>
|
||||
</div>
|
||||
<p className="title-text">Member list</p>
|
||||
{userList.map((user) => (
|
||||
<div key={user.id} className="user-container">
|
||||
<span className="field-text id-text">{user.id}</span>
|
||||
<span className="field-text">{user.email}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PreferencesSection;
|
@ -1,12 +1,12 @@
|
||||
import { useContext, useState } from "react";
|
||||
import appContext from "../stores/appContext";
|
||||
import { userService } from "../services";
|
||||
import utils from "../helpers/utils";
|
||||
import { validate, ValidatorConfig } from "../helpers/validator";
|
||||
import toastHelper from "./Toast";
|
||||
import showChangePasswordDialog from "./ChangePasswordDialog";
|
||||
import showConfirmResetOpenIdDialog from "./ConfirmResetOpenIdDialog";
|
||||
import "../less/my-account-section.less";
|
||||
import appContext from "../../stores/appContext";
|
||||
import { userService } from "../../services";
|
||||
import utils from "../../helpers/utils";
|
||||
import { validate, ValidatorConfig } from "../../helpers/validator";
|
||||
import toastHelper from "../Toast";
|
||||
import showChangePasswordDialog from "../ChangePasswordDialog";
|
||||
import showConfirmResetOpenIdDialog from "../ConfirmResetOpenIdDialog";
|
||||
import "../../less/settings/my-account-section.less";
|
||||
|
||||
const validateConfig: ValidatorConfig = {
|
||||
minLength: 4,
|
78
web/src/components/Settings/PreferencesSection.tsx
Normal file
78
web/src/components/Settings/PreferencesSection.tsx
Normal file
@ -0,0 +1,78 @@
|
||||
import { memoService } from "../../services";
|
||||
import utils from "../../helpers/utils";
|
||||
import toastHelper from "../Toast";
|
||||
import "../../less/settings/preferences-section.less";
|
||||
|
||||
interface Props {}
|
||||
|
||||
const PreferencesSection: React.FC<Props> = () => {
|
||||
const handleExportBtnClick = async () => {
|
||||
const formatedMemos = memoService.getState().memos.map((m) => {
|
||||
return {
|
||||
content: m.content,
|
||||
createdAt: m.createdAt,
|
||||
};
|
||||
});
|
||||
|
||||
const jsonStr = JSON.stringify(formatedMemos);
|
||||
const element = document.createElement("a");
|
||||
element.setAttribute("href", "data:text/json;charset=utf-8," + encodeURIComponent(jsonStr));
|
||||
element.setAttribute("download", `memos-${utils.getDateTimeString(Date.now())}.json`);
|
||||
element.style.display = "none";
|
||||
document.body.appendChild(element);
|
||||
element.click();
|
||||
document.body.removeChild(element);
|
||||
};
|
||||
|
||||
const handleImportBtnClick = async () => {
|
||||
const fileInputEl = document.createElement("input");
|
||||
fileInputEl.type = "file";
|
||||
fileInputEl.accept = "application/JSON";
|
||||
fileInputEl.onchange = () => {
|
||||
if (fileInputEl.files?.length && fileInputEl.files.length > 0) {
|
||||
const reader = new FileReader();
|
||||
reader.readAsText(fileInputEl.files[0]);
|
||||
reader.onload = async (event) => {
|
||||
const memoList = JSON.parse(event.target?.result as string) as Model.Memo[];
|
||||
if (!Array.isArray(memoList)) {
|
||||
toastHelper.error("Unexpected data type.");
|
||||
}
|
||||
|
||||
let succeedAmount = 0;
|
||||
|
||||
for (const memo of memoList) {
|
||||
const content = memo.content || "";
|
||||
const createdAt = memo.createdAt || utils.getDateTimeString(Date.now());
|
||||
|
||||
try {
|
||||
await memoService.importMemo(content, createdAt);
|
||||
succeedAmount++;
|
||||
} catch (error) {
|
||||
// do nth
|
||||
}
|
||||
}
|
||||
|
||||
await memoService.fetchAllMemos();
|
||||
toastHelper.success(`${succeedAmount} memos successfully imported.`);
|
||||
};
|
||||
}
|
||||
};
|
||||
fileInputEl.click();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="section-container preferences-section-container">
|
||||
<p className="title-text">Others</p>
|
||||
<div className="btns-container">
|
||||
<button className="btn" onClick={handleExportBtnClick}>
|
||||
Export data as JSON
|
||||
</button>
|
||||
<button className="btn" onClick={handleImportBtnClick}>
|
||||
Import from JSON
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PreferencesSection;
|
@ -48,10 +48,18 @@ namespace api {
|
||||
});
|
||||
}
|
||||
|
||||
export function getUserInfo() {
|
||||
return request<Model.User>({
|
||||
export function getUserList() {
|
||||
return request<Model.User[]>({
|
||||
method: "GET",
|
||||
url: "/api/user/me",
|
||||
url: "/api/user",
|
||||
});
|
||||
}
|
||||
|
||||
export function createUser(userCreate: API.UserCreate) {
|
||||
return request<Model.User[]>({
|
||||
method: "POST",
|
||||
url: "/api/user",
|
||||
data: userCreate,
|
||||
});
|
||||
}
|
||||
|
||||
@ -66,15 +74,15 @@ namespace api {
|
||||
});
|
||||
}
|
||||
|
||||
export function signup(email: string, role: UserRole, name: string, password: string) {
|
||||
export function signup(email: string, password: string, role: UserRole) {
|
||||
return request<Model.User>({
|
||||
method: "POST",
|
||||
url: "/api/auth/signup",
|
||||
data: {
|
||||
email,
|
||||
role,
|
||||
name,
|
||||
password,
|
||||
role,
|
||||
name: email,
|
||||
},
|
||||
});
|
||||
}
|
||||
@ -86,23 +94,10 @@ namespace api {
|
||||
});
|
||||
}
|
||||
|
||||
export function checkUsernameUsable(name: string) {
|
||||
return request<boolean>({
|
||||
method: "POST",
|
||||
url: "/api/user/rename_check",
|
||||
data: {
|
||||
name,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function checkPasswordValid(password: string) {
|
||||
return request<boolean>({
|
||||
method: "POST",
|
||||
url: "/api/user/password_check",
|
||||
data: {
|
||||
password,
|
||||
},
|
||||
export function getUserInfo() {
|
||||
return request<Model.User>({
|
||||
method: "GET",
|
||||
url: "/api/user/me",
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -29,15 +29,11 @@
|
||||
|
||||
.btn {
|
||||
.flex(column, center, center);
|
||||
@apply w-6 h-6 rounded;
|
||||
@apply w-6 h-6 rounded hover:bg-gray-200 hover:shadow;
|
||||
|
||||
> .icon-img {
|
||||
@apply w-5 h-5;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
@apply bg-gray-200;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -66,10 +66,6 @@ a {
|
||||
|
||||
.btn {
|
||||
@apply select-none cursor-pointer text-center;
|
||||
border: unset;
|
||||
background-color: unset;
|
||||
text-align: unset;
|
||||
font-size: unset;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
|
@ -1,91 +0,0 @@
|
||||
@import "./mixin.less";
|
||||
|
||||
.account-section-container {
|
||||
> .form-label {
|
||||
min-height: 28px;
|
||||
|
||||
> .normal-text {
|
||||
@apply first:mr-2;
|
||||
}
|
||||
|
||||
&.username-label {
|
||||
> input {
|
||||
flex-grow: 0;
|
||||
width: 128px;
|
||||
padding: 0 8px;
|
||||
font-size: 14px;
|
||||
border: 1px solid lightgray;
|
||||
border-radius: 4px;
|
||||
line-height: 26px;
|
||||
background-color: transparent;
|
||||
|
||||
&:focus {
|
||||
border-color: black;
|
||||
}
|
||||
}
|
||||
|
||||
> .btns-container {
|
||||
.flex(row, flex-start, center);
|
||||
margin-left: 8px;
|
||||
flex-shrink: 0;
|
||||
|
||||
> .btn {
|
||||
font-size: 12px;
|
||||
padding: 0 16px;
|
||||
border-radius: 4px;
|
||||
line-height: 28px;
|
||||
margin-right: 8px;
|
||||
background-color: lightgray;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
&.cancel-btn {
|
||||
background-color: unset;
|
||||
}
|
||||
|
||||
&.confirm-btn {
|
||||
background-color: @text-green;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.password-label {
|
||||
> .btn {
|
||||
@apply text-blue-600 ml-1 cursor-pointer hover:opacity-80;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.openapi-section-container {
|
||||
> .value-text {
|
||||
width: 100%;
|
||||
border: 1px solid lightgray;
|
||||
padding: 4px 6px;
|
||||
border-radius: 4px;
|
||||
line-height: 1.6;
|
||||
word-break: break-all;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
> .reset-btn {
|
||||
@apply mt-2 py-1 px-2 bg-red-50 border border-red-500 text-red-600 rounded leading-4 cursor-pointer text-xs select-none hover:opacity-80;
|
||||
}
|
||||
|
||||
> .usage-guide-container {
|
||||
.flex(column, flex-start, flex-start);
|
||||
@apply mt-2 w-full;
|
||||
|
||||
> .title-text {
|
||||
@apply my-2 text-sm;
|
||||
}
|
||||
|
||||
> pre {
|
||||
@apply w-full bg-gray-50 py-2 px-3 text-sm rounded whitespace-pre-wrap;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,44 +0,0 @@
|
||||
@import "./mixin.less";
|
||||
|
||||
.preferences-section-container {
|
||||
> .demo-content-container {
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
border: 2px solid @bg-gray;
|
||||
margin: 12px 0;
|
||||
}
|
||||
|
||||
> .form-label {
|
||||
min-height: 28px;
|
||||
cursor: pointer;
|
||||
|
||||
> .icon-img {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin: 0 8px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
> .btn-container {
|
||||
.flex(row, flex-start, center);
|
||||
width: 100%;
|
||||
margin: 4px 0;
|
||||
|
||||
.btn {
|
||||
height: 28px;
|
||||
padding: 0 12px;
|
||||
margin-right: 8px;
|
||||
border: 1px solid gray;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -3,7 +3,7 @@
|
||||
|
||||
.setting-dialog {
|
||||
> .dialog-container {
|
||||
@apply w-168 max-w-full mb-8 p-0;
|
||||
@apply w-176 max-w-full mb-8 p-0;
|
||||
|
||||
> .dialog-content-container {
|
||||
.flex(column, flex-start, flex-start);
|
||||
@ -12,7 +12,7 @@
|
||||
|
||||
> .close-btn {
|
||||
.flex(column, center, center);
|
||||
@apply absolute top-4 right-4 w-6 h-6 rounded hover:bg-gray-200;
|
||||
@apply absolute top-4 right-4 w-6 h-6 rounded hover:bg-gray-200 hover:shadow;
|
||||
|
||||
> .icon-img {
|
||||
@apply w-5 h-5;
|
||||
@ -20,10 +20,14 @@
|
||||
}
|
||||
|
||||
> .section-selector-container {
|
||||
@apply w-40 h-full shrink-0 rounded-l-lg p-4 bg-gray-100 flex flex-col justify-start items-start;
|
||||
@apply w-40 h-full shrink-0 rounded-l-lg p-4 border-r bg-gray-100 flex flex-col justify-start items-start;
|
||||
|
||||
> .section-title {
|
||||
@apply text-sm mt-4 first:mt-3 mb-1 font-mono text-gray-400;
|
||||
}
|
||||
|
||||
> .section-item {
|
||||
@apply text-base left-6 mt-2 mb-1 cursor-pointer hover:opacity-80;
|
||||
@apply text-base left-6 mt-2 text-gray-700 cursor-pointer hover:opacity-80;
|
||||
|
||||
&.selected {
|
||||
@apply font-bold hover:opacity-100;
|
||||
@ -32,20 +36,19 @@
|
||||
}
|
||||
|
||||
> .section-content-container {
|
||||
@apply w-auto p-4 grow flex flex-col justify-start items-start;
|
||||
@apply w-auto p-4 px-6 grow flex flex-col justify-start items-start h-128 overflow-y-scroll;
|
||||
|
||||
> .section-container {
|
||||
.flex(column, flex-start, flex-start);
|
||||
@apply w-full my-2;
|
||||
|
||||
> .title-text {
|
||||
@apply text-base font-bold mb-2;
|
||||
color: @text-black;
|
||||
@apply text-sm mb-3 font-mono text-gray-500;
|
||||
}
|
||||
|
||||
> .form-label {
|
||||
.flex(row, flex-start, center);
|
||||
@apply w-full text-sm mb-2;
|
||||
@apply w-full mb-2;
|
||||
|
||||
> .normal-text {
|
||||
@apply shrink-0 select-text;
|
||||
|
39
web/src/less/settings/member-section.less
Normal file
39
web/src/less/settings/member-section.less
Normal file
@ -0,0 +1,39 @@
|
||||
@import "../mixin.less";
|
||||
|
||||
.member-section-container {
|
||||
> .create-member-container {
|
||||
@apply w-full flex flex-col justify-start items-start;
|
||||
|
||||
> .input-form-container {
|
||||
@apply w-full mb-2 flex flex-row justify-start items-center;
|
||||
|
||||
> .field-text {
|
||||
@apply text-sm text-gray-600 w-20 text-right pr-2;
|
||||
}
|
||||
|
||||
> input {
|
||||
@apply border rounded text-sm leading-6 shadow-inner py-1 px-2;
|
||||
}
|
||||
}
|
||||
|
||||
> .btns-container {
|
||||
@apply w-full mb-6 pl-20 flex flex-row justify-start items-center;
|
||||
|
||||
> button {
|
||||
@apply border text-sm py-1 px-3 rounded leading-6 shadow hover:opacity-80;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .user-container {
|
||||
@apply w-full mb-4 grid grid-cols-5;
|
||||
|
||||
> .field-text {
|
||||
@apply text-base mr-4 w-16;
|
||||
|
||||
&.id-text {
|
||||
@apply font-mono;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
63
web/src/less/settings/my-account-section.less
Normal file
63
web/src/less/settings/my-account-section.less
Normal file
@ -0,0 +1,63 @@
|
||||
@import "../mixin.less";
|
||||
|
||||
.account-section-container {
|
||||
> .form-label {
|
||||
min-height: 28px;
|
||||
|
||||
> .normal-text {
|
||||
@apply first:mr-2 text-base;
|
||||
}
|
||||
|
||||
&.username-label {
|
||||
> input {
|
||||
@apply grow-0 shadow-inner w-auto px-2 py-1 text-base border rounded leading-6 bg-transparent focus:border-black;
|
||||
}
|
||||
|
||||
> .btns-container {
|
||||
.flex(row, flex-start, center);
|
||||
@apply ml-2 shrink-0;
|
||||
|
||||
> .btn {
|
||||
@apply text-sm shadow px-4 py-1 leading-6 rounded border hover:opacity-80 bg-gray-50;
|
||||
|
||||
&.cancel-btn {
|
||||
@apply shadow-none bg-transparent;
|
||||
}
|
||||
|
||||
&.confirm-btn {
|
||||
@apply bg-green-600 text-white;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.password-label {
|
||||
> .btn {
|
||||
@apply text-blue-600 ml-1 cursor-pointer hover:opacity-80;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.openapi-section-container {
|
||||
> .value-text {
|
||||
@apply w-full font-mono text-sm shadow-inner border py-2 px-3 rounded leading-6 break-all whitespace-pre-wrap;
|
||||
}
|
||||
|
||||
> .reset-btn {
|
||||
@apply mt-2 py-1 px-2 text-sm shadow bg-red-50 border border-red-500 text-red-600 rounded cursor-pointer select-none hover:opacity-80;
|
||||
}
|
||||
|
||||
> .usage-guide-container {
|
||||
.flex(column, flex-start, flex-start);
|
||||
@apply mt-2 w-full;
|
||||
|
||||
> .title-text {
|
||||
@apply my-2 text-sm;
|
||||
}
|
||||
|
||||
> pre {
|
||||
@apply w-full bg-gray-100 shadow-inner py-2 px-3 text-sm rounded font-mono break-all whitespace-pre-wrap;
|
||||
}
|
||||
}
|
||||
}
|
12
web/src/less/settings/preferences-section.less
Normal file
12
web/src/less/settings/preferences-section.less
Normal file
@ -0,0 +1,12 @@
|
||||
@import "../mixin.less";
|
||||
|
||||
.preferences-section-container {
|
||||
> .btns-container {
|
||||
.flex(row, flex-start, center);
|
||||
@apply w-full;
|
||||
|
||||
> .btn {
|
||||
@apply border text-sm py-1 px-3 mr-2 rounded leading-6 shadow hover:opacity-80;
|
||||
}
|
||||
}
|
||||
}
|
@ -12,7 +12,7 @@
|
||||
}
|
||||
|
||||
> .tag {
|
||||
@apply text-xs px-1 bg-blue-500 rounded text-white;
|
||||
@apply text-xs px-1 bg-blue-600 rounded text-white shadow;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -89,11 +89,9 @@ const Signin: React.FC<Props> = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const name = email.split("@")[0];
|
||||
|
||||
try {
|
||||
actionBtnLoadingState.setLoading();
|
||||
await api.signup(email, "OWNER", name, password);
|
||||
await api.signup(email, password, "OWNER");
|
||||
const user = await userService.doSignIn();
|
||||
if (user) {
|
||||
locationService.replaceHistory("/");
|
||||
|
7
web/src/types/api.d.ts
vendored
7
web/src/types/api.d.ts
vendored
@ -2,4 +2,11 @@ declare namespace API {
|
||||
interface SystemStatus {
|
||||
owner: Model.User;
|
||||
}
|
||||
|
||||
interface UserCreate {
|
||||
email: string;
|
||||
password: string;
|
||||
name: string;
|
||||
role: UserRole;
|
||||
}
|
||||
}
|
||||
|
@ -16,6 +16,7 @@ module.exports = {
|
||||
spacing: {
|
||||
128: "32rem",
|
||||
168: "42rem",
|
||||
176: "44rem",
|
||||
200: "50rem",
|
||||
},
|
||||
zIndex: {
|
||||
|
Loading…
Reference in New Issue
Block a user