feat: member manage section in setting dialog

This commit is contained in:
boojack
2022-05-16 22:19:39 +08:00
parent fbf4afff8e
commit c492317ffe
24 changed files with 421 additions and 344 deletions

View File

@@ -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
} }

View File

@@ -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)
} }

View File

@@ -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())

View File

@@ -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

View File

@@ -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 {

View File

@@ -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}

View File

@@ -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;

View File

@@ -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>

View 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;

View File

@@ -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,

View 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;

View File

@@ -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,
},
}); });
} }

View File

@@ -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;
}
} }
} }

View File

@@ -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 {

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}
}
}

View File

@@ -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;

View 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;
}
}
}
}

View 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;
}
}
}

View 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;
}
}
}

View File

@@ -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;
} }
} }

View File

@@ -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("/");

View File

@@ -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;
}
} }

View File

@@ -16,6 +16,7 @@ module.exports = {
spacing: { spacing: {
128: "32rem", 128: "32rem",
168: "42rem", 168: "42rem",
176: "44rem",
200: "50rem", 200: "50rem",
}, },
zIndex: { zIndex: {