mirror of
https://github.com/usememos/memos.git
synced 2025-06-05 22:09:59 +02:00
feat: member manage section in setting dialog
This commit is contained in:
@@ -27,9 +27,10 @@ type User struct {
|
|||||||
|
|
||||||
type UserCreate struct {
|
type UserCreate struct {
|
||||||
// Domain specific fields
|
// Domain specific fields
|
||||||
Email string
|
Email string `json:"email"`
|
||||||
Role Role
|
Role Role `json:"role"`
|
||||||
Name string
|
Name string `json:"name"`
|
||||||
|
Password string `json:"password"`
|
||||||
PasswordHash string
|
PasswordHash string
|
||||||
OpenID string
|
OpenID string
|
||||||
}
|
}
|
||||||
|
@@ -81,24 +81,10 @@ func (s *Server) registerAuthRoutes(g *echo.Group) {
|
|||||||
if len(signup.Email) < 6 {
|
if len(signup.Email) < 6 {
|
||||||
return echo.NewHTTPError(http.StatusBadRequest, "Email is too short, minimum length is 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 {
|
if len(signup.Password) < 6 {
|
||||||
return echo.NewHTTPError(http.StatusBadRequest, "Password is too short, minimum length is 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)
|
passwordHash, err := bcrypt.GenerateFromPassword([]byte(signup.Password), bcrypt.DefaultCost)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to generate password hash").SetInternal(err)
|
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),
|
PasswordHash: string(passwordHash),
|
||||||
OpenID: common.GenUUID(),
|
OpenID: common.GenUUID(),
|
||||||
}
|
}
|
||||||
user, err = s.Store.CreateUser(userCreate)
|
user, err := s.Store.CreateUser(userCreate)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create user").SetInternal(err)
|
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create user").SetInternal(err)
|
||||||
}
|
}
|
||||||
|
@@ -12,6 +12,45 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func (s *Server) registerUserRoutes(g *echo.Group) {
|
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.
|
// GET /api/user/me is used to check if the user is logged in.
|
||||||
g.GET("/user/me", func(c echo.Context) error {
|
g.GET("/user/me", func(c echo.Context) error {
|
||||||
userSessionID := c.Get(getUserIDContextKey())
|
userSessionID := c.Get(getUserIDContextKey())
|
||||||
|
@@ -1,14 +1,13 @@
|
|||||||
-- user
|
-- user
|
||||||
CREATE TABLE user (
|
CREATE TABLE user (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
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',
|
role TEXT NOT NULL CHECK (role IN ('OWNER', 'USER')) DEFAULT 'USER',
|
||||||
name TEXT NOT NULL,
|
name TEXT NOT NULL,
|
||||||
password_hash 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')),
|
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
|
||||||
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
|
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now'))
|
||||||
UNIQUE(`email`, `open_id`)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
INSERT INTO
|
INSERT INTO
|
||||||
|
@@ -25,6 +25,15 @@ func (s *Store) PatchUser(patch *api.UserPatch) (*api.User, error) {
|
|||||||
return user, nil
|
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) {
|
func (s *Store) FindUser(find *api.UserFind) (*api.User, error) {
|
||||||
list, err := findUserList(s.db, find)
|
list, err := findUserList(s.db, find)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@@ -246,7 +246,7 @@ const MemoEditor: React.FC<Props> = () => {
|
|||||||
const file = inputEl.files[0];
|
const file = inputEl.files[0];
|
||||||
const url = await handleUploadFile(file);
|
const url = await handleUploadFile(file);
|
||||||
if (url) {
|
if (url) {
|
||||||
editorRef.current?.insertText(url);
|
editorRef.current?.insertText(url + " ");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
inputEl.click();
|
inputEl.click();
|
||||||
@@ -259,7 +259,7 @@ const MemoEditor: React.FC<Props> = () => {
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const showEditStatus = Boolean(globalState.editMemoId);
|
const isEditing = Boolean(globalState.editMemoId);
|
||||||
|
|
||||||
const editorConfig = useMemo(
|
const editorConfig = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
@@ -267,17 +267,17 @@ const MemoEditor: React.FC<Props> = () => {
|
|||||||
initialContent: getEditorContentCache(),
|
initialContent: getEditorContentCache(),
|
||||||
placeholder: "Any thoughts...",
|
placeholder: "Any thoughts...",
|
||||||
showConfirmBtn: true,
|
showConfirmBtn: true,
|
||||||
showCancelBtn: showEditStatus,
|
showCancelBtn: isEditing,
|
||||||
onConfirmBtnClick: handleSaveBtnClick,
|
onConfirmBtnClick: handleSaveBtnClick,
|
||||||
onCancelBtnClick: handleCancelBtnClick,
|
onCancelBtnClick: handleCancelBtnClick,
|
||||||
onContentChange: handleContentChange,
|
onContentChange: handleContentChange,
|
||||||
}),
|
}),
|
||||||
[showEditStatus]
|
[isEditing]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={"memo-editor-container " + (showEditStatus ? "edit-ing" : "")}>
|
<div className={"memo-editor-container " + (isEditing ? "edit-ing" : "")}>
|
||||||
<p className={"tip-text " + (showEditStatus ? "" : "hidden")}>Editting...</p>
|
<p className={"tip-text " + (isEditing ? "" : "hidden")}>Editting...</p>
|
||||||
<Editor
|
<Editor
|
||||||
ref={editorRef}
|
ref={editorRef}
|
||||||
{...editorConfig}
|
{...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 { showDialog } from "./Dialog";
|
||||||
import MyAccountSection from "./MyAccountSection";
|
import MyAccountSection from "./Settings/MyAccountSection";
|
||||||
import PreferencesSection from "./PreferencesSection";
|
import PreferencesSection from "./Settings/PreferencesSection";
|
||||||
|
import MemberSection from "./Settings/MemberSection";
|
||||||
import "../less/setting-dialog.less";
|
import "../less/setting-dialog.less";
|
||||||
|
|
||||||
interface Props extends DialogProps {}
|
interface Props extends DialogProps {}
|
||||||
|
|
||||||
type SettingSection = "my-account" | "preferences";
|
type SettingSection = "my-account" | "preferences" | "member";
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
selectedSection: SettingSection;
|
selectedSection: SettingSection;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SettingDialog: React.FC<Props> = (props: Props) => {
|
const SettingDialog: React.FC<Props> = (props: Props) => {
|
||||||
|
const {
|
||||||
|
userState: { user },
|
||||||
|
} = useContext(appContext);
|
||||||
const { destroy } = props;
|
const { destroy } = props;
|
||||||
const [state, setState] = useState<State>({
|
const [state, setState] = useState<State>({
|
||||||
selectedSection: "my-account",
|
selectedSection: "my-account",
|
||||||
@@ -30,6 +35,7 @@ const SettingDialog: React.FC<Props> = (props: Props) => {
|
|||||||
<img className="icon-img" src="/icons/close.svg" />
|
<img className="icon-img" src="/icons/close.svg" />
|
||||||
</button>
|
</button>
|
||||||
<div className="section-selector-container">
|
<div className="section-selector-container">
|
||||||
|
<span className="section-title">Basic</span>
|
||||||
<span
|
<span
|
||||||
onClick={() => handleSectionSelectorItemClick("my-account")}
|
onClick={() => handleSectionSelectorItemClick("my-account")}
|
||||||
className={`section-item ${state.selectedSection === "my-account" ? "selected" : ""}`}
|
className={`section-item ${state.selectedSection === "my-account" ? "selected" : ""}`}
|
||||||
@@ -42,12 +48,25 @@ const SettingDialog: React.FC<Props> = (props: Props) => {
|
|||||||
>
|
>
|
||||||
Preferences
|
Preferences
|
||||||
</span>
|
</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>
|
||||||
<div className="section-content-container">
|
<div className="section-content-container">
|
||||||
{state.selectedSection === "my-account" ? (
|
{state.selectedSection === "my-account" ? (
|
||||||
<MyAccountSection />
|
<MyAccountSection />
|
||||||
) : state.selectedSection === "preferences" ? (
|
) : state.selectedSection === "preferences" ? (
|
||||||
<PreferencesSection />
|
<PreferencesSection />
|
||||||
|
) : state.selectedSection === "member" ? (
|
||||||
|
<MemberSection />
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</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 { useContext, useState } from "react";
|
||||||
import appContext from "../stores/appContext";
|
import appContext from "../../stores/appContext";
|
||||||
import { userService } from "../services";
|
import { userService } from "../../services";
|
||||||
import utils from "../helpers/utils";
|
import utils from "../../helpers/utils";
|
||||||
import { validate, ValidatorConfig } from "../helpers/validator";
|
import { validate, ValidatorConfig } from "../../helpers/validator";
|
||||||
import toastHelper from "./Toast";
|
import toastHelper from "../Toast";
|
||||||
import showChangePasswordDialog from "./ChangePasswordDialog";
|
import showChangePasswordDialog from "../ChangePasswordDialog";
|
||||||
import showConfirmResetOpenIdDialog from "./ConfirmResetOpenIdDialog";
|
import showConfirmResetOpenIdDialog from "../ConfirmResetOpenIdDialog";
|
||||||
import "../less/my-account-section.less";
|
import "../../less/settings/my-account-section.less";
|
||||||
|
|
||||||
const validateConfig: ValidatorConfig = {
|
const validateConfig: ValidatorConfig = {
|
||||||
minLength: 4,
|
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() {
|
export function getUserList() {
|
||||||
return request<Model.User>({
|
return request<Model.User[]>({
|
||||||
method: "GET",
|
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>({
|
return request<Model.User>({
|
||||||
method: "POST",
|
method: "POST",
|
||||||
url: "/api/auth/signup",
|
url: "/api/auth/signup",
|
||||||
data: {
|
data: {
|
||||||
email,
|
email,
|
||||||
role,
|
|
||||||
name,
|
|
||||||
password,
|
password,
|
||||||
|
role,
|
||||||
|
name: email,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -86,23 +94,10 @@ namespace api {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function checkUsernameUsable(name: string) {
|
export function getUserInfo() {
|
||||||
return request<boolean>({
|
return request<Model.User>({
|
||||||
method: "POST",
|
method: "GET",
|
||||||
url: "/api/user/rename_check",
|
url: "/api/user/me",
|
||||||
data: {
|
|
||||||
name,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function checkPasswordValid(password: string) {
|
|
||||||
return request<boolean>({
|
|
||||||
method: "POST",
|
|
||||||
url: "/api/user/password_check",
|
|
||||||
data: {
|
|
||||||
password,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -29,15 +29,11 @@
|
|||||||
|
|
||||||
.btn {
|
.btn {
|
||||||
.flex(column, center, center);
|
.flex(column, center, center);
|
||||||
@apply w-6 h-6 rounded;
|
@apply w-6 h-6 rounded hover:bg-gray-200 hover:shadow;
|
||||||
|
|
||||||
> .icon-img {
|
> .icon-img {
|
||||||
@apply w-5 h-5;
|
@apply w-5 h-5;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
|
||||||
@apply bg-gray-200;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -66,10 +66,6 @@ a {
|
|||||||
|
|
||||||
.btn {
|
.btn {
|
||||||
@apply select-none cursor-pointer text-center;
|
@apply select-none cursor-pointer text-center;
|
||||||
border: unset;
|
|
||||||
background-color: unset;
|
|
||||||
text-align: unset;
|
|
||||||
font-size: unset;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.hidden {
|
.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 {
|
.setting-dialog {
|
||||||
> .dialog-container {
|
> .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 {
|
> .dialog-content-container {
|
||||||
.flex(column, flex-start, flex-start);
|
.flex(column, flex-start, flex-start);
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
|
|
||||||
> .close-btn {
|
> .close-btn {
|
||||||
.flex(column, center, center);
|
.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 {
|
> .icon-img {
|
||||||
@apply w-5 h-5;
|
@apply w-5 h-5;
|
||||||
@@ -20,10 +20,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
> .section-selector-container {
|
> .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 {
|
> .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 {
|
&.selected {
|
||||||
@apply font-bold hover:opacity-100;
|
@apply font-bold hover:opacity-100;
|
||||||
@@ -32,20 +36,19 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
> .section-content-container {
|
> .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 {
|
> .section-container {
|
||||||
.flex(column, flex-start, flex-start);
|
.flex(column, flex-start, flex-start);
|
||||||
@apply w-full my-2;
|
@apply w-full my-2;
|
||||||
|
|
||||||
> .title-text {
|
> .title-text {
|
||||||
@apply text-base font-bold mb-2;
|
@apply text-sm mb-3 font-mono text-gray-500;
|
||||||
color: @text-black;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
> .form-label {
|
> .form-label {
|
||||||
.flex(row, flex-start, center);
|
.flex(row, flex-start, center);
|
||||||
@apply w-full text-sm mb-2;
|
@apply w-full mb-2;
|
||||||
|
|
||||||
> .normal-text {
|
> .normal-text {
|
||||||
@apply shrink-0 select-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 {
|
> .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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const name = email.split("@")[0];
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
actionBtnLoadingState.setLoading();
|
actionBtnLoadingState.setLoading();
|
||||||
await api.signup(email, "OWNER", name, password);
|
await api.signup(email, password, "OWNER");
|
||||||
const user = await userService.doSignIn();
|
const user = await userService.doSignIn();
|
||||||
if (user) {
|
if (user) {
|
||||||
locationService.replaceHistory("/");
|
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 {
|
interface SystemStatus {
|
||||||
owner: Model.User;
|
owner: Model.User;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface UserCreate {
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
name: string;
|
||||||
|
role: UserRole;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -16,6 +16,7 @@ module.exports = {
|
|||||||
spacing: {
|
spacing: {
|
||||||
128: "32rem",
|
128: "32rem",
|
||||||
168: "42rem",
|
168: "42rem",
|
||||||
|
176: "44rem",
|
||||||
200: "50rem",
|
200: "50rem",
|
||||||
},
|
},
|
||||||
zIndex: {
|
zIndex: {
|
||||||
|
Reference in New Issue
Block a user