Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| eea826dd5c |
@@ -0,0 +1,37 @@
|
||||
name: build-and-push-dev-image
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "main"
|
||||
|
||||
jobs:
|
||||
build-and-push-dev-image:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: neosmemo
|
||||
password: ${{ secrets.DOCKER_NEOSMEMO_TOKEN }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
with:
|
||||
install: true
|
||||
|
||||
- name: Build and Push
|
||||
id: docker_build
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
context: ./
|
||||
file: ./Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: neosmemo/memos:dev
|
||||
@@ -3,13 +3,13 @@
|
||||
<p align="center">An open source, self-hosted knowledge base that works with a SQLite db file.</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/usememos/memos/stargazers"><img alt="GitHub stars" src="https://img.shields.io/github/stars/usememos/memos" /></a>
|
||||
<img alt="GitHub stars" src="https://img.shields.io/github/stars/usememos/memos" />
|
||||
<a href="https://hub.docker.com/r/neosmemo/memos"><img alt="Docker pull" src="https://img.shields.io/docker/pulls/neosmemo/memos.svg" /></a>
|
||||
<img alt="Go report" src="https://goreportcard.com/badge/github.com/usememos/memos" />
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://demo.usememos.com/">Live Demo</a> •
|
||||
<a href="https://memos.onrender.com/">Live Demo</a> •
|
||||
<a href="https://t.me/+-_tNF1k70UU4ZTc9">Discuss in Telegram 👾</a>
|
||||
</p>
|
||||
|
||||
@@ -36,7 +36,9 @@ docker run \
|
||||
--port 5230
|
||||
```
|
||||
|
||||
Memos should be running at [http://localhost:5230](http://localhost:5230). If the `~/.memos/` does not have a `memos_prod.db` file, then memos will auto generate it.
|
||||
Memos should now be running at [http://localhost:5230](http://localhost:5230). If the `~/.memos/` does not have a `memos_prod.db` file, then `memos` will auto generate it.
|
||||
|
||||
⚠️ Please DO NOT use `dev` tag of docker image if you have no experience.
|
||||
|
||||
## 🏗 Development
|
||||
|
||||
@@ -52,9 +54,8 @@ Memos is built with a curated tech stack. It is optimized for developer experien
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- [Go](https://golang.org/doc/install)
|
||||
- [Go](https://golang.org/doc/install) (1.16 or later)
|
||||
- [Air](https://github.com/cosmtrek/air#installation) for backend live reload
|
||||
- [Node.js](https://nodejs.org/)
|
||||
- [yarn](https://yarnpkg.com/getting-started/install)
|
||||
|
||||
### Steps
|
||||
@@ -79,10 +80,10 @@ Memos is built with a curated tech stack. It is optimized for developer experien
|
||||
|
||||
Memos should now be running at [http://localhost:3000](http://localhost:3000) and change either frontend or backend code would trigger live reload.
|
||||
|
||||
### Contributing
|
||||
|
||||
Contributions are what make the open source community such an amazing place to be learn, inspire, and create. Any contributions you make are greatly appreciated. 🥰
|
||||
|
||||
## 🌟 Star history
|
||||
|
||||
[](https://star-history.com/#usememos/memos&Date)
|
||||
|
||||
---
|
||||
|
||||
Just enjoy it.
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
package api
|
||||
|
||||
var (
|
||||
UNKNOWN_ID = 0
|
||||
)
|
||||
|
||||
type Signin struct {
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password"`
|
||||
|
||||
+3
-7
@@ -6,8 +6,6 @@ type Visibility string
|
||||
const (
|
||||
// Public is the PUBLIC visibility.
|
||||
Public Visibility = "PUBLIC"
|
||||
// Protected is the PROTECTED visibility.
|
||||
Protected Visibility = "PROTECTED"
|
||||
// Privite is the PRIVATE visibility.
|
||||
Privite Visibility = "PRIVATE"
|
||||
)
|
||||
@@ -16,8 +14,6 @@ func (e Visibility) String() string {
|
||||
switch e {
|
||||
case Public:
|
||||
return "PUBLIC"
|
||||
case Protected:
|
||||
return "PROTECTED"
|
||||
case Privite:
|
||||
return "PRIVATE"
|
||||
}
|
||||
@@ -69,9 +65,9 @@ type MemoFind struct {
|
||||
CreatorID *int `json:"creatorId"`
|
||||
|
||||
// Domain specific fields
|
||||
Pinned *bool
|
||||
ContentSearch *string
|
||||
VisibilityList []Visibility
|
||||
Pinned *bool
|
||||
ContentSearch *string
|
||||
Visibility *Visibility
|
||||
|
||||
// Pagination
|
||||
Limit int
|
||||
|
||||
@@ -38,7 +38,4 @@ type ResourceFind struct {
|
||||
|
||||
type ResourceDelete struct {
|
||||
ID int
|
||||
|
||||
// Standard fields
|
||||
CreatorID int
|
||||
}
|
||||
|
||||
@@ -73,7 +73,3 @@ type UserFind struct {
|
||||
Name *string `json:"name"`
|
||||
OpenID *string
|
||||
}
|
||||
|
||||
type UserDelete struct {
|
||||
ID int
|
||||
}
|
||||
|
||||
+2
-2
@@ -7,10 +7,10 @@ import (
|
||||
|
||||
// Version is the service current released version.
|
||||
// Semantic versioning: https://semver.org/
|
||||
var Version = "0.3.1"
|
||||
var Version = "0.2.2"
|
||||
|
||||
// DevVersion is the service current development version.
|
||||
var DevVersion = "0.3.1"
|
||||
var DevVersion = "0.2.2"
|
||||
|
||||
func GetCurrentVersion(mode string) string {
|
||||
if mode == "dev" {
|
||||
|
||||
Executable → Regular
+1
-4
@@ -1,7 +1,4 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Usage: ./scripts/build.sh
|
||||
|
||||
# Usage: sh ./scripts/build.sh
|
||||
set -e
|
||||
|
||||
cd "$(dirname "$0")/../"
|
||||
|
||||
-116
@@ -1,116 +0,0 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/usememos/memos/api"
|
||||
"github.com/usememos/memos/common"
|
||||
|
||||
"github.com/gorilla/sessions"
|
||||
"github.com/labstack/echo-contrib/session"
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
var (
|
||||
userIDContextKey = "user-id"
|
||||
)
|
||||
|
||||
func getUserIDContextKey() string {
|
||||
return userIDContextKey
|
||||
}
|
||||
|
||||
func setUserSession(ctx echo.Context, user *api.User) error {
|
||||
sess, _ := session.Get("session", ctx)
|
||||
sess.Options = &sessions.Options{
|
||||
Path: "/",
|
||||
MaxAge: 1000 * 3600 * 24 * 30,
|
||||
HttpOnly: true,
|
||||
}
|
||||
sess.Values[userIDContextKey] = user.ID
|
||||
err := sess.Save(ctx.Request(), ctx.Response())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to set session, err: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func removeUserSession(ctx echo.Context) error {
|
||||
sess, _ := session.Get("session", ctx)
|
||||
sess.Options = &sessions.Options{
|
||||
Path: "/",
|
||||
MaxAge: 0,
|
||||
HttpOnly: true,
|
||||
}
|
||||
sess.Values[userIDContextKey] = nil
|
||||
err := sess.Save(ctx.Request(), ctx.Response())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to set session, err: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func aclMiddleware(s *Server, next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(ctx echo.Context) error {
|
||||
// Skip auth.
|
||||
if common.HasPrefixes(ctx.Path(), "/api/auth") {
|
||||
return next(ctx)
|
||||
}
|
||||
|
||||
if common.HasPrefixes(ctx.Path(), "/api/ping", "/api/status", "/api/user/:id") && ctx.Request().Method == http.MethodGet {
|
||||
return next(ctx)
|
||||
}
|
||||
|
||||
// If there is openId in query string and related user is found, then skip auth.
|
||||
openID := ctx.QueryParam("openId")
|
||||
if openID != "" {
|
||||
userFind := &api.UserFind{
|
||||
OpenID: &openID,
|
||||
}
|
||||
user, err := s.Store.FindUser(userFind)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user by open_id").SetInternal(err)
|
||||
}
|
||||
if user != nil {
|
||||
// Stores userID into context.
|
||||
ctx.Set(getUserIDContextKey(), user.ID)
|
||||
return next(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
sess, _ := session.Get("session", ctx)
|
||||
userIDValue := sess.Values[userIDContextKey]
|
||||
if userIDValue != nil {
|
||||
userID, _ := strconv.Atoi(fmt.Sprintf("%v", userIDValue))
|
||||
userFind := &api.UserFind{
|
||||
ID: &userID,
|
||||
}
|
||||
user, err := s.Store.FindUser(userFind)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to find user by ID: %d", userID)).SetInternal(err)
|
||||
}
|
||||
if user != nil {
|
||||
if user.RowStatus == api.Archived {
|
||||
return echo.NewHTTPError(http.StatusForbidden, fmt.Sprintf("User has been archived with email %s", user.Email))
|
||||
}
|
||||
ctx.Set(getUserIDContextKey(), userID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if common.HasPrefixes(ctx.Path(), "/api/memo", "/api/tag", "/api/shortcut") && ctx.Request().Method == http.MethodGet {
|
||||
if _, err := strconv.Atoi(ctx.QueryParam("creatorId")); err == nil {
|
||||
return next(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
userID := ctx.Get(getUserIDContextKey())
|
||||
if userID == nil {
|
||||
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
|
||||
}
|
||||
|
||||
return next(ctx)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/usememos/memos/api"
|
||||
"github.com/usememos/memos/common"
|
||||
|
||||
"github.com/gorilla/sessions"
|
||||
"github.com/labstack/echo-contrib/session"
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
var (
|
||||
userIDContextKey = "user-id"
|
||||
)
|
||||
|
||||
func getUserIDContextKey() string {
|
||||
return userIDContextKey
|
||||
}
|
||||
|
||||
func setUserSession(c echo.Context, user *api.User) error {
|
||||
sess, _ := session.Get("session", c)
|
||||
sess.Options = &sessions.Options{
|
||||
Path: "/",
|
||||
MaxAge: 1000 * 3600 * 24 * 30,
|
||||
HttpOnly: true,
|
||||
}
|
||||
sess.Values[userIDContextKey] = user.ID
|
||||
err := sess.Save(c.Request(), c.Response())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to set session, err: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func removeUserSession(c echo.Context) error {
|
||||
sess, _ := session.Get("session", c)
|
||||
sess.Options = &sessions.Options{
|
||||
Path: "/",
|
||||
MaxAge: 0,
|
||||
HttpOnly: true,
|
||||
}
|
||||
sess.Values[userIDContextKey] = nil
|
||||
err := sess.Save(c.Request(), c.Response())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to set session, err: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Use session to store user.id.
|
||||
func BasicAuthMiddleware(s *Server, next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
// Skip auth for some paths.
|
||||
if common.HasPrefixes(c.Path(), "/api/auth", "/api/ping", "/api/status", "/api/user/:userId") {
|
||||
return next(c)
|
||||
}
|
||||
|
||||
// If there is openId in query string and related user is found, then skip auth.
|
||||
openID := c.QueryParam("openId")
|
||||
if openID != "" {
|
||||
userFind := &api.UserFind{
|
||||
OpenID: &openID,
|
||||
}
|
||||
user, err := s.Store.FindUser(userFind)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user by open_id").SetInternal(err)
|
||||
}
|
||||
if user != nil {
|
||||
// Stores userID into context.
|
||||
c.Set(getUserIDContextKey(), user.ID)
|
||||
return next(c)
|
||||
}
|
||||
}
|
||||
|
||||
if common.HasPrefixes(c.Path(), "/api/memo", "/api/tag", "/api/shortcut") && c.Request().Method == http.MethodGet {
|
||||
if _, err := strconv.Atoi(c.QueryParam("creatorId")); err == nil {
|
||||
return next(c)
|
||||
}
|
||||
}
|
||||
|
||||
sess, err := session.Get("session", c)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusUnauthorized, "Missing session").SetInternal(err)
|
||||
}
|
||||
|
||||
userIDValue := sess.Values[userIDContextKey]
|
||||
if userIDValue == nil {
|
||||
return echo.NewHTTPError(http.StatusUnauthorized, "Missing userID in session")
|
||||
}
|
||||
|
||||
userID, err := strconv.Atoi(fmt.Sprintf("%v", userIDValue))
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to malformatted user id in the session.").SetInternal(err)
|
||||
}
|
||||
|
||||
// Even if there is no error, we still need to make sure the user still exists.
|
||||
userFind := &api.UserFind{
|
||||
ID: &userID,
|
||||
}
|
||||
user, err := s.Store.FindUser(userFind)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to find user by ID: %d", userID)).SetInternal(err)
|
||||
}
|
||||
if user == nil {
|
||||
return echo.NewHTTPError(http.StatusUnauthorized, fmt.Sprintf("Not found user ID: %d", userID))
|
||||
} else if user.RowStatus == api.Archived {
|
||||
return echo.NewHTTPError(http.StatusForbidden, fmt.Sprintf("User has been archived with email %s", user.Email))
|
||||
}
|
||||
|
||||
// Stores userID into context.
|
||||
c.Set(getUserIDContextKey(), userID)
|
||||
|
||||
return next(c)
|
||||
}
|
||||
}
|
||||
+18
-34
@@ -5,7 +5,6 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/usememos/memos/api"
|
||||
"github.com/usememos/memos/common"
|
||||
@@ -15,10 +14,7 @@ import (
|
||||
|
||||
func (s *Server) registerMemoRoutes(g *echo.Group) {
|
||||
g.POST("/memo", func(c echo.Context) error {
|
||||
userID, ok := c.Get(getUserIDContextKey()).(int)
|
||||
if !ok {
|
||||
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
|
||||
}
|
||||
userID := c.Get(getUserIDContextKey()).(int)
|
||||
memoCreate := &api.MemoCreate{
|
||||
CreatorID: userID,
|
||||
}
|
||||
@@ -73,20 +69,20 @@ func (s *Server) registerMemoRoutes(g *echo.Group) {
|
||||
|
||||
if userID, err := strconv.Atoi(c.QueryParam("creatorId")); err == nil {
|
||||
memoFind.CreatorID = &userID
|
||||
}
|
||||
|
||||
currentUserID, ok := c.Get(getUserIDContextKey()).(int)
|
||||
if !ok {
|
||||
if memoFind.CreatorID == nil {
|
||||
} else {
|
||||
userID, ok := c.Get(getUserIDContextKey()).(int)
|
||||
if !ok {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "Missing user id to find memo")
|
||||
}
|
||||
memoFind.VisibilityList = []api.Visibility{api.Public}
|
||||
} else {
|
||||
if memoFind.CreatorID == nil {
|
||||
memoFind.CreatorID = ¤tUserID
|
||||
} else {
|
||||
memoFind.VisibilityList = []api.Visibility{api.Public, api.Protected}
|
||||
}
|
||||
|
||||
memoFind.CreatorID = &userID
|
||||
}
|
||||
|
||||
// Only can get PUBLIC memos in visitor mode
|
||||
_, ok := c.Get(getUserIDContextKey()).(int)
|
||||
if !ok {
|
||||
publicVisibility := api.Public
|
||||
memoFind.Visibility = &publicVisibility
|
||||
}
|
||||
|
||||
rowStatus := api.RowStatus(c.QueryParam("rowStatus"))
|
||||
@@ -103,14 +99,6 @@ func (s *Server) registerMemoRoutes(g *echo.Group) {
|
||||
contentSearch := "#" + tag + " "
|
||||
memoFind.ContentSearch = &contentSearch
|
||||
}
|
||||
visibilitListStr := c.QueryParam("visibility")
|
||||
if visibilitListStr != "" {
|
||||
visibilityList := []api.Visibility{}
|
||||
for _, visibility := range strings.Split(visibilitListStr, ",") {
|
||||
visibilityList = append(visibilityList, api.Visibility(visibility))
|
||||
}
|
||||
memoFind.VisibilityList = visibilityList
|
||||
}
|
||||
if limit, err := strconv.Atoi(c.QueryParam("limit")); err == nil {
|
||||
memoFind.Limit = limit
|
||||
}
|
||||
@@ -136,10 +124,7 @@ func (s *Server) registerMemoRoutes(g *echo.Group) {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("memoId"))).SetInternal(err)
|
||||
}
|
||||
|
||||
userID, ok := c.Get(getUserIDContextKey()).(int)
|
||||
if !ok {
|
||||
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
|
||||
}
|
||||
userID := c.Get(getUserIDContextKey()).(int)
|
||||
memoOrganizerUpsert := &api.MemoOrganizerUpsert{
|
||||
MemoID: memoID,
|
||||
UserID: userID,
|
||||
@@ -205,7 +190,9 @@ func (s *Server) registerMemoRoutes(g *echo.Group) {
|
||||
memoDelete := &api.MemoDelete{
|
||||
ID: memoID,
|
||||
}
|
||||
if err := s.Store.DeleteMemo(memoDelete); err != nil {
|
||||
|
||||
err = s.Store.DeleteMemo(memoDelete)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to delete memo ID: %v", memoID)).SetInternal(err)
|
||||
}
|
||||
|
||||
@@ -213,10 +200,7 @@ func (s *Server) registerMemoRoutes(g *echo.Group) {
|
||||
})
|
||||
|
||||
g.GET("/memo/amount", func(c echo.Context) error {
|
||||
userID, ok := c.Get(getUserIDContextKey()).(int)
|
||||
if !ok {
|
||||
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
|
||||
}
|
||||
userID := c.Get(getUserIDContextKey()).(int)
|
||||
normalRowStatus := api.Normal
|
||||
memoFind := &api.MemoFind{
|
||||
CreatorID: &userID,
|
||||
|
||||
+5
-23
@@ -14,10 +14,7 @@ import (
|
||||
|
||||
func (s *Server) registerResourceRoutes(g *echo.Group) {
|
||||
g.POST("/resource", func(c echo.Context) error {
|
||||
userID, ok := c.Get(getUserIDContextKey()).(int)
|
||||
if !ok {
|
||||
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
|
||||
}
|
||||
userID := c.Get(getUserIDContextKey()).(int)
|
||||
|
||||
err := c.Request().ParseMultipartForm(64 << 20)
|
||||
if err != nil {
|
||||
@@ -64,10 +61,7 @@ func (s *Server) registerResourceRoutes(g *echo.Group) {
|
||||
})
|
||||
|
||||
g.GET("/resource", func(c echo.Context) error {
|
||||
userID, ok := c.Get(getUserIDContextKey()).(int)
|
||||
if !ok {
|
||||
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
|
||||
}
|
||||
userID := c.Get(getUserIDContextKey()).(int)
|
||||
resourceFind := &api.ResourceFind{
|
||||
CreatorID: &userID,
|
||||
}
|
||||
@@ -89,10 +83,7 @@ func (s *Server) registerResourceRoutes(g *echo.Group) {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("resourceId"))).SetInternal(err)
|
||||
}
|
||||
|
||||
userID, ok := c.Get(getUserIDContextKey()).(int)
|
||||
if !ok {
|
||||
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
|
||||
}
|
||||
userID := c.Get(getUserIDContextKey()).(int)
|
||||
resourceFind := &api.ResourceFind{
|
||||
ID: &resourceID,
|
||||
CreatorID: &userID,
|
||||
@@ -115,10 +106,7 @@ func (s *Server) registerResourceRoutes(g *echo.Group) {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("resourceId"))).SetInternal(err)
|
||||
}
|
||||
|
||||
userID, ok := c.Get(getUserIDContextKey()).(int)
|
||||
if !ok {
|
||||
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
|
||||
}
|
||||
userID := c.Get(getUserIDContextKey()).(int)
|
||||
resourceFind := &api.ResourceFind{
|
||||
ID: &resourceID,
|
||||
CreatorID: &userID,
|
||||
@@ -138,19 +126,13 @@ func (s *Server) registerResourceRoutes(g *echo.Group) {
|
||||
})
|
||||
|
||||
g.DELETE("/resource/:resourceId", func(c echo.Context) error {
|
||||
userID, ok := c.Get(getUserIDContextKey()).(int)
|
||||
if !ok {
|
||||
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
|
||||
}
|
||||
|
||||
resourceID, err := strconv.Atoi(c.Param("resourceId"))
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("resourceId"))).SetInternal(err)
|
||||
}
|
||||
|
||||
resourceDelete := &api.ResourceDelete{
|
||||
ID: resourceID,
|
||||
CreatorID: userID,
|
||||
ID: resourceID,
|
||||
}
|
||||
if err := s.Store.DeleteResource(resourceDelete); err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to delete resource").SetInternal(err)
|
||||
|
||||
+1
-3
@@ -32,8 +32,6 @@ func NewServer(profile *profile.Profile) *Server {
|
||||
Format: "${method} ${uri} ${status}\n",
|
||||
}))
|
||||
|
||||
e.Use(middleware.CORS())
|
||||
|
||||
e.Use(middleware.TimeoutWithConfig(middleware.TimeoutConfig{
|
||||
Skipper: middleware.DefaultSkipper,
|
||||
ErrorMessage: "Request timeout",
|
||||
@@ -60,7 +58,7 @@ func NewServer(profile *profile.Profile) *Server {
|
||||
|
||||
apiGroup := e.Group("/api")
|
||||
apiGroup.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return aclMiddleware(s, next)
|
||||
return BasicAuthMiddleware(s, next)
|
||||
})
|
||||
s.registerSystemRoutes(apiGroup)
|
||||
s.registerAuthRoutes(apiGroup)
|
||||
|
||||
+1
-4
@@ -13,10 +13,7 @@ import (
|
||||
|
||||
func (s *Server) registerShortcutRoutes(g *echo.Group) {
|
||||
g.POST("/shortcut", func(c echo.Context) error {
|
||||
userID, ok := c.Get(getUserIDContextKey()).(int)
|
||||
if !ok {
|
||||
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
|
||||
}
|
||||
userID := c.Get(getUserIDContextKey()).(int)
|
||||
shortcutCreate := &api.ShortcutCreate{
|
||||
CreatorID: userID,
|
||||
}
|
||||
|
||||
+11
-11
@@ -22,21 +22,21 @@ func (s *Server) registerTagRoutes(g *echo.Group) {
|
||||
}
|
||||
|
||||
if userID, err := strconv.Atoi(c.QueryParam("creatorId")); err == nil {
|
||||
memoFind.CreatorID = &userID
|
||||
} else {
|
||||
userID, ok := c.Get(getUserIDContextKey()).(int)
|
||||
if !ok {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "Missing user id to find tag")
|
||||
}
|
||||
|
||||
memoFind.CreatorID = &userID
|
||||
}
|
||||
|
||||
currentUserID, ok := c.Get(getUserIDContextKey()).(int)
|
||||
// Only can get PUBLIC memos in visitor mode
|
||||
_, ok := c.Get(getUserIDContextKey()).(int)
|
||||
if !ok {
|
||||
if memoFind.CreatorID == nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "Missing user id to find memo")
|
||||
}
|
||||
memoFind.VisibilityList = []api.Visibility{api.Public}
|
||||
} else {
|
||||
if memoFind.CreatorID == nil {
|
||||
memoFind.CreatorID = ¤tUserID
|
||||
} else {
|
||||
memoFind.VisibilityList = []api.Visibility{api.Public, api.Protected}
|
||||
}
|
||||
publicVisibility := api.Public
|
||||
memoFind.Visibility = &publicVisibility
|
||||
}
|
||||
|
||||
memoList, err := s.Store.FindMemoList(&memoFind)
|
||||
|
||||
+32
-39
@@ -44,11 +44,6 @@ func (s *Server) registerUserRoutes(g *echo.Group) {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch user list").SetInternal(err)
|
||||
}
|
||||
|
||||
for _, user := range userList {
|
||||
// data desensitize
|
||||
user.OpenID = ""
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -83,11 +78,12 @@ func (s *Server) registerUserRoutes(g *echo.Group) {
|
||||
|
||||
// GET /api/user/me is used to check if the user is logged in.
|
||||
g.GET("/user/me", func(c echo.Context) error {
|
||||
userID, ok := c.Get(getUserIDContextKey()).(int)
|
||||
if !ok {
|
||||
userSessionID := c.Get(getUserIDContextKey())
|
||||
if userSessionID == nil {
|
||||
return echo.NewHTTPError(http.StatusUnauthorized, "Missing auth session")
|
||||
}
|
||||
|
||||
userID := userSessionID.(int)
|
||||
userFind := &api.UserFind{
|
||||
ID: &userID,
|
||||
}
|
||||
@@ -103,27 +99,8 @@ func (s *Server) registerUserRoutes(g *echo.Group) {
|
||||
return nil
|
||||
})
|
||||
|
||||
g.PATCH("/user/:id", func(c echo.Context) error {
|
||||
userID, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("id"))).SetInternal(err)
|
||||
}
|
||||
currentUserID, ok := c.Get(getUserIDContextKey()).(int)
|
||||
if !ok {
|
||||
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
|
||||
}
|
||||
currentUser, err := s.Store.FindUser(&api.UserFind{
|
||||
ID: ¤tUserID,
|
||||
})
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user").SetInternal(err)
|
||||
}
|
||||
if currentUser == nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Current session user not found with ID: %d", currentUserID)).SetInternal(err)
|
||||
} else if currentUser.Role != api.Host && currentUserID != userID {
|
||||
return echo.NewHTTPError(http.StatusForbidden, "Access forbidden for current session user").SetInternal(err)
|
||||
}
|
||||
|
||||
g.PATCH("/user/me", func(c echo.Context) error {
|
||||
userID := c.Get(getUserIDContextKey()).(int)
|
||||
userPatch := &api.UserPatch{
|
||||
ID: userID,
|
||||
}
|
||||
@@ -158,11 +135,8 @@ func (s *Server) registerUserRoutes(g *echo.Group) {
|
||||
return nil
|
||||
})
|
||||
|
||||
g.DELETE("/user/:id", func(c echo.Context) error {
|
||||
currentUserID, ok := c.Get(getUserIDContextKey()).(int)
|
||||
if !ok {
|
||||
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
|
||||
}
|
||||
g.PATCH("/user/:userId", func(c echo.Context) error {
|
||||
currentUserID := c.Get(getUserIDContextKey()).(int)
|
||||
currentUser, err := s.Store.FindUser(&api.UserFind{
|
||||
ID: ¤tUserID,
|
||||
})
|
||||
@@ -175,18 +149,37 @@ func (s *Server) registerUserRoutes(g *echo.Group) {
|
||||
return echo.NewHTTPError(http.StatusForbidden, "Access forbidden for current session user").SetInternal(err)
|
||||
}
|
||||
|
||||
userID, err := strconv.Atoi(c.Param("id"))
|
||||
userID, err := strconv.Atoi(c.Param("userId"))
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("id"))).SetInternal(err)
|
||||
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("userId"))).SetInternal(err)
|
||||
}
|
||||
|
||||
userDelete := &api.UserDelete{
|
||||
userPatch := &api.UserPatch{
|
||||
ID: userID,
|
||||
}
|
||||
if err := s.Store.DeleteUser(userDelete); err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to delete user").SetInternal(err)
|
||||
if err := json.NewDecoder(c.Request().Body).Decode(userPatch); err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted patch user request").SetInternal(err)
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, true)
|
||||
if userPatch.Password != nil && *userPatch.Password != "" {
|
||||
passwordHash, err := bcrypt.GenerateFromPassword([]byte(*userPatch.Password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to generate password hash").SetInternal(err)
|
||||
}
|
||||
|
||||
passwordHashStr := string(passwordHash)
|
||||
userPatch.PasswordHash = &passwordHashStr
|
||||
}
|
||||
|
||||
user, err := s.Store.PatchUser(userPatch)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to patch 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
|
||||
})
|
||||
}
|
||||
|
||||
+3
-3
@@ -128,7 +128,7 @@ const (
|
||||
)
|
||||
|
||||
func (db *DB) applyLatestSchema() error {
|
||||
latestSchemaPath := fmt.Sprintf("%s/%s/%s", "migration", db.profile.Mode, latestSchemaFileName)
|
||||
latestSchemaPath := fmt.Sprintf("%s/%s", "migration", latestSchemaFileName)
|
||||
buf, err := migrationFS.ReadFile(latestSchemaPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read latest schema %q, error %w", latestSchemaPath, err)
|
||||
@@ -141,7 +141,7 @@ func (db *DB) applyLatestSchema() error {
|
||||
}
|
||||
|
||||
func (db *DB) applyMigrationForMinorVersion(minorVersion string) error {
|
||||
filenames, err := fs.Glob(migrationFS, fmt.Sprintf("%s/%s/*.sql", "migration/prod", minorVersion))
|
||||
filenames, err := fs.Glob(migrationFS, fmt.Sprintf("%s/%s/*.sql", "migration", minorVersion))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -210,7 +210,7 @@ func (db *DB) execute(stmt string) error {
|
||||
}
|
||||
|
||||
// minorDirRegexp is a regular expression for minor version directory.
|
||||
var minorDirRegexp = regexp.MustCompile(`^migration/prod/[0-9]+\.[0-9]+$`)
|
||||
var minorDirRegexp = regexp.MustCompile(`^migration/[0-9]+\.[0-9]+$`)
|
||||
|
||||
func getMinorVersionList() []string {
|
||||
minorVersionList := []string{}
|
||||
|
||||
+2
-1
@@ -4,7 +4,8 @@ PRAGMA foreign_keys = off;
|
||||
|
||||
DROP TABLE IF EXISTS _user_old;
|
||||
|
||||
ALTER TABLE user RENAME TO _user_old;
|
||||
ALTER TABLE
|
||||
user RENAME TO _user_old;
|
||||
|
||||
CREATE TABLE user (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
@@ -44,7 +44,7 @@ CREATE TABLE memo (
|
||||
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
|
||||
row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL',
|
||||
content TEXT NOT NULL DEFAULT '',
|
||||
visibility TEXT NOT NULL CHECK (visibility IN ('PUBLIC', 'PROTECTED', 'PRIVATE')) DEFAULT 'PRIVATE',
|
||||
visibility TEXT NOT NULL CHECK (visibility IN ('PUBLIC', 'PRIVATE')) DEFAULT 'PRIVATE',
|
||||
FOREIGN KEY(creator_id) REFERENCES user(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
-- change memo visibility field from "PRIVATE"/"PUBLIC" to "PRIVATE"/"PROTECTED"/"PUBLIC".
|
||||
|
||||
PRAGMA foreign_keys = off;
|
||||
|
||||
DROP TABLE IF EXISTS _memo_old;
|
||||
|
||||
ALTER TABLE memo RENAME TO _memo_old;
|
||||
|
||||
CREATE TABLE memo (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
creator_id INTEGER NOT NULL,
|
||||
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
|
||||
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
|
||||
row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL',
|
||||
content TEXT NOT NULL DEFAULT '',
|
||||
visibility TEXT NOT NULL CHECK (visibility IN ('PUBLIC', 'PROTECTED', 'PRIVATE')) DEFAULT 'PRIVATE',
|
||||
FOREIGN KEY(creator_id) REFERENCES user(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
INSERT INTO memo (
|
||||
id, creator_id, created_ts, updated_ts,
|
||||
row_status, content, visibility
|
||||
)
|
||||
SELECT
|
||||
id,
|
||||
creator_id,
|
||||
created_ts,
|
||||
updated_ts,
|
||||
row_status,
|
||||
content,
|
||||
visibility
|
||||
FROM
|
||||
_memo_old;
|
||||
|
||||
DROP TABLE IF EXISTS _memo_old;
|
||||
|
||||
PRAGMA foreign_keys = on;
|
||||
@@ -1,141 +0,0 @@
|
||||
-- drop all tables
|
||||
DROP TABLE IF EXISTS `memo_organizer`;
|
||||
DROP TABLE IF EXISTS `memo`;
|
||||
DROP TABLE IF EXISTS `shortcut`;
|
||||
DROP TABLE IF EXISTS `resource`;
|
||||
DROP TABLE IF EXISTS `user`;
|
||||
|
||||
-- user
|
||||
CREATE TABLE user (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
|
||||
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
|
||||
-- allowed row status are 'NORMAL', 'ARCHIVED'.
|
||||
row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL',
|
||||
email TEXT NOT NULL UNIQUE,
|
||||
role TEXT NOT NULL CHECK (role IN ('HOST', 'USER')) DEFAULT 'USER',
|
||||
name TEXT NOT NULL,
|
||||
password_hash TEXT NOT NULL,
|
||||
open_id TEXT NOT NULL UNIQUE
|
||||
);
|
||||
|
||||
INSERT INTO
|
||||
sqlite_sequence (name, seq)
|
||||
VALUES
|
||||
('user', 100);
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS `trigger_update_user_modification_time`
|
||||
AFTER
|
||||
UPDATE
|
||||
ON `user` FOR EACH ROW BEGIN
|
||||
UPDATE
|
||||
`user`
|
||||
SET
|
||||
updated_ts = (strftime('%s', 'now'))
|
||||
WHERE
|
||||
rowid = old.rowid;
|
||||
END;
|
||||
|
||||
-- memo
|
||||
CREATE TABLE memo (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
creator_id INTEGER NOT NULL,
|
||||
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
|
||||
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
|
||||
row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL',
|
||||
content TEXT NOT NULL DEFAULT '',
|
||||
visibility TEXT NOT NULL CHECK (visibility IN ('PUBLIC', 'PROTECTED', 'PRIVATE')) DEFAULT 'PRIVATE',
|
||||
FOREIGN KEY(creator_id) REFERENCES user(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
INSERT INTO
|
||||
sqlite_sequence (name, seq)
|
||||
VALUES
|
||||
('memo', 1000);
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS `trigger_update_memo_modification_time`
|
||||
AFTER
|
||||
UPDATE
|
||||
ON `memo` FOR EACH ROW BEGIN
|
||||
UPDATE
|
||||
`memo`
|
||||
SET
|
||||
updated_ts = (strftime('%s', 'now'))
|
||||
WHERE
|
||||
rowid = old.rowid;
|
||||
END;
|
||||
|
||||
-- memo_organizer
|
||||
CREATE TABLE memo_organizer (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
memo_id INTEGER NOT NULL,
|
||||
user_id INTEGER NOT NULL,
|
||||
pinned INTEGER NOT NULL CHECK (pinned IN (0, 1)) DEFAULT 0,
|
||||
FOREIGN KEY(memo_id) REFERENCES memo(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY(user_id) REFERENCES user(id) ON DELETE CASCADE,
|
||||
UNIQUE(memo_id, user_id)
|
||||
);
|
||||
|
||||
INSERT INTO
|
||||
sqlite_sequence (name, seq)
|
||||
VALUES
|
||||
('memo_organizer', 1000);
|
||||
|
||||
-- shortcut
|
||||
CREATE TABLE shortcut (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
creator_id INTEGER NOT NULL,
|
||||
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
|
||||
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
|
||||
row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL',
|
||||
title TEXT NOT NULL DEFAULT '',
|
||||
payload TEXT NOT NULL DEFAULT '{}',
|
||||
FOREIGN KEY(creator_id) REFERENCES user(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
INSERT INTO
|
||||
sqlite_sequence (name, seq)
|
||||
VALUES
|
||||
('shortcut', 10000);
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS `trigger_update_shortcut_modification_time`
|
||||
AFTER
|
||||
UPDATE
|
||||
ON `shortcut` FOR EACH ROW BEGIN
|
||||
UPDATE
|
||||
`shortcut`
|
||||
SET
|
||||
updated_ts = (strftime('%s', 'now'))
|
||||
WHERE
|
||||
rowid = old.rowid;
|
||||
END;
|
||||
|
||||
-- resource
|
||||
CREATE TABLE resource (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
creator_id INTEGER NOT NULL,
|
||||
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
|
||||
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
|
||||
filename TEXT NOT NULL DEFAULT '',
|
||||
blob BLOB NOT NULL,
|
||||
type TEXT NOT NULL DEFAULT '',
|
||||
size INTEGER NOT NULL DEFAULT 0,
|
||||
FOREIGN KEY(creator_id) REFERENCES user(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
INSERT INTO
|
||||
sqlite_sequence (name, seq)
|
||||
VALUES
|
||||
('resource', 10000);
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS `trigger_update_resource_modification_time`
|
||||
AFTER
|
||||
UPDATE
|
||||
ON `resource` FOR EACH ROW BEGIN
|
||||
UPDATE
|
||||
`resource`
|
||||
SET
|
||||
updated_ts = (strftime('%s', 'now'))
|
||||
WHERE
|
||||
rowid = old.rowid;
|
||||
END;
|
||||
@@ -37,25 +37,3 @@ VALUES
|
||||
-- raw password: secret
|
||||
'$2a$14$ajq8Q7fbtFRQvXpdCq7Jcuy.Rx1h/L4J60Otx.gyNLbAYctGMJ9tK'
|
||||
);
|
||||
|
||||
INSERT INTO
|
||||
user (
|
||||
`id`,
|
||||
`row_status`,
|
||||
`email`,
|
||||
`role`,
|
||||
`name`,
|
||||
`open_id`,
|
||||
`password_hash`
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
103,
|
||||
'ARCHIVED',
|
||||
'bob@usememos.com',
|
||||
'USER',
|
||||
'Bob',
|
||||
'bob_open_id',
|
||||
-- raw password: secret
|
||||
'$2a$14$ajq8Q7fbtFRQvXpdCq7Jcuy.Rx1h/L4J60Otx.gyNLbAYctGMJ9tK'
|
||||
);
|
||||
@@ -16,8 +16,7 @@ INSERT INTO
|
||||
memo (
|
||||
`id`,
|
||||
`content`,
|
||||
`creator_id`,
|
||||
`visibility`
|
||||
`creator_id`
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
@@ -27,8 +26,7 @@ VALUES
|
||||
- [x] Clean the room;
|
||||
- [x] Read *📖 The Little Prince*;
|
||||
(👆 click to toggle status)',
|
||||
101,
|
||||
'PROTECTED'
|
||||
101
|
||||
);
|
||||
|
||||
INSERT INTO
|
||||
@@ -50,8 +48,7 @@ INSERT INTO
|
||||
memo (
|
||||
`id`,
|
||||
`content`,
|
||||
`creator_id`,
|
||||
`visibility`
|
||||
`creator_id`
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
@@ -62,8 +59,7 @@ VALUES
|
||||
- [ ] Watch *👦 The Boys*;
|
||||
(👆 click to toggle status)
|
||||
',
|
||||
102,
|
||||
'PROTECTED'
|
||||
102
|
||||
);
|
||||
|
||||
INSERT INTO
|
||||
|
||||
+3
-11
@@ -222,13 +222,8 @@ func findMemoRawList(db *sql.DB, find *api.MemoFind) ([]*memoRaw, error) {
|
||||
if v := find.ContentSearch; v != nil {
|
||||
where, args = append(where, "content LIKE ?"), append(args, "%"+*v+"%")
|
||||
}
|
||||
if v := find.VisibilityList; len(v) != 0 {
|
||||
list := []string{}
|
||||
for _, visibility := range v {
|
||||
list = append(list, fmt.Sprintf("$%d", len(args)+1))
|
||||
args = append(args, visibility)
|
||||
}
|
||||
where = append(where, fmt.Sprintf("visibility in (%s)", strings.Join(list, ",")))
|
||||
if v := find.Visibility; v != nil {
|
||||
where, args = append(where, "visibility = ?"), append(args, *v)
|
||||
}
|
||||
|
||||
pagination := ""
|
||||
@@ -284,10 +279,7 @@ func findMemoRawList(db *sql.DB, find *api.MemoFind) ([]*memoRaw, error) {
|
||||
}
|
||||
|
||||
func deleteMemo(db *sql.DB, delete *api.MemoDelete) error {
|
||||
result, err := db.Exec(`
|
||||
PRAGMA foreign_keys = ON;
|
||||
DELETE FROM memo WHERE id = ?
|
||||
`, delete.ID)
|
||||
result, err := db.Exec(`DELETE FROM memo WHERE id = ?`, delete.ID)
|
||||
if err != nil {
|
||||
return FormatError(err)
|
||||
}
|
||||
|
||||
+2
-8
@@ -102,7 +102,7 @@ func createResource(db *sql.DB, create *api.ResourceCreate) (*resourceRaw, error
|
||||
creator_id
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
RETURNING id, filename, blob, type, size, creator_id, created_ts, updated_ts
|
||||
RETURNING id, filename, blob, type, size, created_ts, updated_ts
|
||||
`,
|
||||
create.Filename,
|
||||
create.Blob,
|
||||
@@ -123,7 +123,6 @@ func createResource(db *sql.DB, create *api.ResourceCreate) (*resourceRaw, error
|
||||
&resourceRaw.Blob,
|
||||
&resourceRaw.Type,
|
||||
&resourceRaw.Size,
|
||||
&resourceRaw.CreatorID,
|
||||
&resourceRaw.CreatedTs,
|
||||
&resourceRaw.UpdatedTs,
|
||||
); err != nil {
|
||||
@@ -153,7 +152,6 @@ func findResourceList(db *sql.DB, find *api.ResourceFind) ([]*resourceRaw, error
|
||||
blob,
|
||||
type,
|
||||
size,
|
||||
creator_id,
|
||||
created_ts,
|
||||
updated_ts
|
||||
FROM resource
|
||||
@@ -175,7 +173,6 @@ func findResourceList(db *sql.DB, find *api.ResourceFind) ([]*resourceRaw, error
|
||||
&resourceRaw.Blob,
|
||||
&resourceRaw.Type,
|
||||
&resourceRaw.Size,
|
||||
&resourceRaw.CreatorID,
|
||||
&resourceRaw.CreatedTs,
|
||||
&resourceRaw.UpdatedTs,
|
||||
); err != nil {
|
||||
@@ -193,10 +190,7 @@ func findResourceList(db *sql.DB, find *api.ResourceFind) ([]*resourceRaw, error
|
||||
}
|
||||
|
||||
func deleteResource(db *sql.DB, delete *api.ResourceDelete) error {
|
||||
result, err := db.Exec(`
|
||||
PRAGMA foreign_keys = ON;
|
||||
DELETE FROM resource WHERE id = ? AND creator_id = ?
|
||||
`, delete.ID, delete.CreatorID)
|
||||
result, err := db.Exec(`DELETE FROM resource WHERE id = ?`, delete.ID)
|
||||
if err != nil {
|
||||
return FormatError(err)
|
||||
}
|
||||
|
||||
+1
-4
@@ -238,10 +238,7 @@ func findShortcutList(db *sql.DB, find *api.ShortcutFind) ([]*shortcutRaw, error
|
||||
}
|
||||
|
||||
func deleteShortcut(db *sql.DB, delete *api.ShortcutDelete) error {
|
||||
result, err := db.Exec(`
|
||||
PRAGMA foreign_keys = ON;
|
||||
DELETE FROM shortcut WHERE id = ?
|
||||
`, delete.ID)
|
||||
result, err := db.Exec(`DELETE FROM shortcut WHERE id = ?`, delete.ID)
|
||||
if err != nil {
|
||||
return FormatError(err)
|
||||
}
|
||||
|
||||
+2
-27
@@ -96,15 +96,6 @@ func (s *Store) FindUser(find *api.UserFind) (*api.User, error) {
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (s *Store) DeleteUser(delete *api.UserDelete) error {
|
||||
err := deleteUser(s.db, delete)
|
||||
if err != nil {
|
||||
return FormatError(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func createUser(db *sql.DB, create *api.UserCreate) (*userRaw, error) {
|
||||
row, err := db.Query(`
|
||||
INSERT INTO user (
|
||||
@@ -233,7 +224,7 @@ func findUserList(db *sql.DB, find *api.UserFind) ([]*userRaw, error) {
|
||||
row_status
|
||||
FROM user
|
||||
WHERE `+strings.Join(where, " AND ")+`
|
||||
ORDER BY created_ts DESC, row_status DESC`,
|
||||
ORDER BY created_ts DESC`,
|
||||
args...,
|
||||
)
|
||||
if err != nil {
|
||||
@@ -255,6 +246,7 @@ func findUserList(db *sql.DB, find *api.UserFind) ([]*userRaw, error) {
|
||||
&userRaw.UpdatedTs,
|
||||
&userRaw.RowStatus,
|
||||
); err != nil {
|
||||
fmt.Println(err)
|
||||
return nil, FormatError(err)
|
||||
}
|
||||
|
||||
@@ -267,20 +259,3 @@ func findUserList(db *sql.DB, find *api.UserFind) ([]*userRaw, error) {
|
||||
|
||||
return userRawList, nil
|
||||
}
|
||||
|
||||
func deleteUser(db *sql.DB, delete *api.UserDelete) error {
|
||||
result, err := db.Exec(`
|
||||
PRAGMA foreign_keys = ON;
|
||||
DELETE FROM user WHERE id = ?
|
||||
`, delete.ID)
|
||||
if err != nil {
|
||||
return FormatError(err)
|
||||
}
|
||||
|
||||
rows, _ := result.RowsAffected()
|
||||
if rows == 0 {
|
||||
return &common.Error{Code: common.NotFound, Err: fmt.Errorf("user ID not found: %d", delete.ID)}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
+3
-2
@@ -2,10 +2,11 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="/logo.png" type="image/*" />
|
||||
<link rel="icon" href="/favicon.svg" sizes="64x64" type="image/*" />
|
||||
<meta name="theme-color" content="#f6f5f4" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no" />
|
||||
<title>Memos</title>
|
||||
<script src="https://kit.fontawesome.com/41e3aaa6af.js" crossorigin="anonymous"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
+1
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "memos",
|
||||
"version": "0.3.1",
|
||||
"version": "0.2.2",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
@@ -15,7 +15,6 @@
|
||||
"qs": "^6.11.0",
|
||||
"react": "^18.1.0",
|
||||
"react-dom": "^18.1.0",
|
||||
"react-feather": "^2.0.10",
|
||||
"react-redux": "^8.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 27 KiB |
+1
-12
@@ -1,21 +1,10 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { appRouterSwitch } from "./routers";
|
||||
import { locationService } from "./services";
|
||||
import { useAppSelector } from "./store";
|
||||
|
||||
function App() {
|
||||
const pathname = useAppSelector((state) => state.location.pathname);
|
||||
const [isLoading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
locationService.updateStateWithLocation();
|
||||
window.onpopstate = () => {
|
||||
locationService.updateStateWithLocation();
|
||||
};
|
||||
setLoading(false);
|
||||
}, []);
|
||||
|
||||
return <>{isLoading ? null : appRouterSwitch(pathname)}</>;
|
||||
return <>{appRouterSwitch(pathname)}</>;
|
||||
}
|
||||
|
||||
export default App;
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import * as api from "../helpers/api";
|
||||
import Only from "./common/OnlyWhen";
|
||||
import Icon from "./Icon";
|
||||
import { generateDialog } from "./Dialog";
|
||||
import { showDialog } from "./Dialog";
|
||||
import GitHubBadge from "./GitHubBadge";
|
||||
import "../less/about-site-dialog.less";
|
||||
|
||||
@@ -38,7 +37,7 @@ const AboutSiteDialog: React.FC<Props> = ({ destroy }: Props) => {
|
||||
<span className="icon-text">🤠</span>About <b>Memos</b>
|
||||
</p>
|
||||
<button className="btn close-btn" onClick={handleCloseBtnClick}>
|
||||
<Icon.X />
|
||||
<i className="fa-solid fa-xmark fa-lg"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div className="dialog-content-container">
|
||||
@@ -64,7 +63,7 @@ const AboutSiteDialog: React.FC<Props> = ({ destroy }: Props) => {
|
||||
};
|
||||
|
||||
export default function showAboutSiteDialog(): void {
|
||||
generateDialog(
|
||||
showDialog(
|
||||
{
|
||||
className: "about-site-dialog",
|
||||
},
|
||||
|
||||
@@ -2,10 +2,10 @@ import { IMAGE_URL_REG } from "../helpers/consts";
|
||||
import * as utils from "../helpers/utils";
|
||||
import useToggle from "../hooks/useToggle";
|
||||
import { memoService } from "../services";
|
||||
import { formatMemoContent } from "../helpers/marked";
|
||||
import Only from "./common/OnlyWhen";
|
||||
import Image from "./Image";
|
||||
import toastHelper from "./Toast";
|
||||
import { formatMemoContent } from "./Memo";
|
||||
import "../less/memo.less";
|
||||
|
||||
interface Props {
|
||||
|
||||
@@ -2,8 +2,7 @@ import { useEffect, useState } from "react";
|
||||
import useLoading from "../hooks/useLoading";
|
||||
import { memoService } from "../services";
|
||||
import { useAppSelector } from "../store";
|
||||
import Icon from "./Icon";
|
||||
import { generateDialog } from "./Dialog";
|
||||
import { showDialog } from "./Dialog";
|
||||
import toastHelper from "./Toast";
|
||||
import ArchivedMemo from "./ArchivedMemo";
|
||||
import "../less/archived-memo-dialog.less";
|
||||
@@ -38,7 +37,7 @@ const ArchivedMemoDialog: React.FC<Props> = (props: Props) => {
|
||||
Archived Memos
|
||||
</p>
|
||||
<button className="btn close-btn" onClick={destroy}>
|
||||
<Icon.X className="icon-img" />
|
||||
<i className="fa-solid fa-xmark fa-lg icon-img"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div className="dialog-content-container">
|
||||
@@ -48,7 +47,7 @@ const ArchivedMemoDialog: React.FC<Props> = (props: Props) => {
|
||||
</div>
|
||||
) : archivedMemos.length === 0 ? (
|
||||
<div className="tip-text-container">
|
||||
<p className="tip-text">No archived memos.</p>
|
||||
<p className="tip-text">Here is No Zettels.</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="archived-memos-container">
|
||||
@@ -62,8 +61,8 @@ const ArchivedMemoDialog: React.FC<Props> = (props: Props) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default function showArchivedMemoDialog(): void {
|
||||
generateDialog(
|
||||
export default function showArchivedMemo(): void {
|
||||
showDialog(
|
||||
{
|
||||
className: "archived-memo-dialog",
|
||||
useAppContext: true,
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { validate, ValidatorConfig } from "../helpers/validator";
|
||||
import { userService } from "../services";
|
||||
import Icon from "./Icon";
|
||||
import { generateDialog } from "./Dialog";
|
||||
import { showDialog } from "./Dialog";
|
||||
import toastHelper from "./Toast";
|
||||
import "../less/change-password-dialog.less";
|
||||
|
||||
@@ -56,9 +55,7 @@ const ChangePasswordDialog: React.FC<Props> = ({ destroy }: Props) => {
|
||||
}
|
||||
|
||||
try {
|
||||
const user = userService.getState().user as User;
|
||||
await userService.patchUser({
|
||||
id: user.id,
|
||||
password: newPassword,
|
||||
});
|
||||
toastHelper.info("Password changed.");
|
||||
@@ -73,7 +70,7 @@ const ChangePasswordDialog: React.FC<Props> = ({ destroy }: Props) => {
|
||||
<div className="dialog-header-container">
|
||||
<p className="title-text">Change Password</p>
|
||||
<button className="btn close-btn" onClick={handleCloseBtnClick}>
|
||||
<Icon.X />
|
||||
<i className="fa-solid fa-xmark fa-lg"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div className="dialog-content-container">
|
||||
@@ -97,7 +94,7 @@ const ChangePasswordDialog: React.FC<Props> = ({ destroy }: Props) => {
|
||||
};
|
||||
|
||||
function showChangePasswordDialog() {
|
||||
generateDialog(
|
||||
showDialog(
|
||||
{
|
||||
className: "change-password-dialog",
|
||||
},
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
import { useEffect } from "react";
|
||||
import { userService } from "../services";
|
||||
import useLoading from "../hooks/useLoading";
|
||||
import { showDialog } from "./Dialog";
|
||||
import toastHelper from "./Toast";
|
||||
import "../less/confirm-reset-openid-dialog.less";
|
||||
|
||||
interface Props extends DialogProps {}
|
||||
|
||||
const ConfirmResetOpenIdDialog: React.FC<Props> = ({ destroy }: Props) => {
|
||||
const resetBtnClickLoadingState = useLoading(false);
|
||||
|
||||
useEffect(() => {
|
||||
// do nth
|
||||
}, []);
|
||||
|
||||
const handleCloseBtnClick = () => {
|
||||
destroy();
|
||||
};
|
||||
|
||||
const handleConfirmBtnClick = async () => {
|
||||
if (resetBtnClickLoadingState.isLoading) {
|
||||
return;
|
||||
}
|
||||
|
||||
resetBtnClickLoadingState.setLoading();
|
||||
try {
|
||||
await userService.patchUser({
|
||||
resetOpenId: true,
|
||||
});
|
||||
} catch (error) {
|
||||
toastHelper.error("Request reset open API failed.");
|
||||
return;
|
||||
}
|
||||
toastHelper.success("Reset open API succeeded.");
|
||||
handleCloseBtnClick();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="dialog-header-container">
|
||||
<p className="title-text">Reset Open API</p>
|
||||
<button className="btn close-btn" onClick={handleCloseBtnClick}>
|
||||
<i className="fa-solid fa-xmark fa-lg"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div className="dialog-content-container">
|
||||
<p className="warn-text">
|
||||
❗️The existing API will be invalidated and a new one will be generated, are you sure you want to reset?
|
||||
</p>
|
||||
<div className="btns-container">
|
||||
<span className="btn cancel-btn" onClick={handleCloseBtnClick}>
|
||||
Cancel
|
||||
</span>
|
||||
<span className={`btn confirm-btn ${resetBtnClickLoadingState.isLoading ? "loading" : ""}`} onClick={handleConfirmBtnClick}>
|
||||
Reset!
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
function showConfirmResetOpenIdDialog() {
|
||||
showDialog(
|
||||
{
|
||||
className: "confirm-reset-openid-dialog",
|
||||
},
|
||||
ConfirmResetOpenIdDialog
|
||||
);
|
||||
}
|
||||
|
||||
export default showConfirmResetOpenIdDialog;
|
||||
@@ -2,8 +2,7 @@ import { memo, useCallback, useEffect, useState } from "react";
|
||||
import { memoService, shortcutService } from "../services";
|
||||
import { checkShouldShowMemoWithFilters, filterConsts, getDefaultFilter, relationConsts } from "../helpers/filter";
|
||||
import useLoading from "../hooks/useLoading";
|
||||
import Icon from "./Icon";
|
||||
import { generateDialog } from "./Dialog";
|
||||
import { showDialog } from "./Dialog";
|
||||
import toastHelper from "./Toast";
|
||||
import Selector from "./common/Selector";
|
||||
import "../less/create-shortcut-dialog.less";
|
||||
@@ -101,7 +100,7 @@ const CreateShortcutDialog: React.FC<Props> = (props: Props) => {
|
||||
{shortcutId ? "Edit Shortcut" : "Create Shortcut"}
|
||||
</p>
|
||||
<button className="btn close-btn" onClick={destroy}>
|
||||
<Icon.X />
|
||||
<i className="fa-solid fa-xmark fa-lg"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div className="dialog-content-container">
|
||||
@@ -297,7 +296,7 @@ const FilterInputer: React.FC<MemoFilterInputerProps> = (props: MemoFilterInpute
|
||||
/>
|
||||
|
||||
{inputElements}
|
||||
<Icon.X className="remove-btn" onClick={handleRemoveBtnClick} />
|
||||
<i className="fa-solid fa-xmark remove-btn" onClick={handleRemoveBtnClick}></i>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -305,7 +304,7 @@ const FilterInputer: React.FC<MemoFilterInputerProps> = (props: MemoFilterInpute
|
||||
const MemoFilterInputer: React.FC<MemoFilterInputerProps> = memo(FilterInputer);
|
||||
|
||||
export default function showCreateShortcutDialog(shortcutId?: ShortcutId): void {
|
||||
generateDialog(
|
||||
showDialog(
|
||||
{
|
||||
className: "create-shortcut-dialog",
|
||||
},
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { IMAGE_URL_REG } from "../helpers/consts";
|
||||
import * as utils from "../helpers/utils";
|
||||
import { formatMemoContent } from "../helpers/marked";
|
||||
import Only from "./common/OnlyWhen";
|
||||
import { formatMemoContent } from "./Memo";
|
||||
import "../less/daily-memo.less";
|
||||
|
||||
interface DailyMemo extends Memo {
|
||||
@@ -18,6 +20,7 @@ const DailyMemo: React.FC<Props> = (props: Props) => {
|
||||
createdAtStr: utils.getDateTimeString(propsMemo.createdTs),
|
||||
timeStr: utils.getTimeString(propsMemo.createdTs),
|
||||
};
|
||||
const imageUrls = Array.from(memo.content.match(IMAGE_URL_REG) ?? []).map((s) => s.replace(IMAGE_URL_REG, "$1"));
|
||||
|
||||
return (
|
||||
<div className="daily-memo-wrapper">
|
||||
@@ -25,14 +28,14 @@ const DailyMemo: React.FC<Props> = (props: Props) => {
|
||||
<span className="normal-text">{memo.timeStr}</span>
|
||||
</div>
|
||||
<div className="memo-content-container">
|
||||
<div
|
||||
className="memo-content-text"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: formatMemoContent(memo.content, {
|
||||
inlineImage: true,
|
||||
}),
|
||||
}}
|
||||
></div>
|
||||
<div className="memo-content-text" dangerouslySetInnerHTML={{ __html: formatMemoContent(memo.content) }}></div>
|
||||
<Only when={imageUrls.length > 0}>
|
||||
<div className="images-container">
|
||||
{imageUrls.map((imgUrl, idx) => (
|
||||
<img key={idx} src={imgUrl} decoding="async" />
|
||||
))}
|
||||
</div>
|
||||
</Only>
|
||||
</div>
|
||||
<div className="split-line"></div>
|
||||
</div>
|
||||
|
||||
@@ -4,8 +4,7 @@ import toImage from "../labs/html2image";
|
||||
import useToggle from "../hooks/useToggle";
|
||||
import { DAILY_TIMESTAMP } from "../helpers/consts";
|
||||
import * as utils from "../helpers/utils";
|
||||
import Icon from "./Icon";
|
||||
import { generateDialog } from "./Dialog";
|
||||
import { showDialog } from "./Dialog";
|
||||
import DatePicker from "./common/DatePicker";
|
||||
import showPreviewImageDialog from "./PreviewImageDialog";
|
||||
import DailyMemo from "./DailyMemo";
|
||||
@@ -65,17 +64,16 @@ const DailyReviewDialog: React.FC<Props> = (props: Props) => {
|
||||
</p>
|
||||
<div className="btns-container">
|
||||
<button className="btn-text" onClick={() => setCurrentDateStamp(currentDateStamp - DAILY_TIMESTAMP)}>
|
||||
<Icon.ChevronLeft className="icon-img" />
|
||||
<i className="fa-solid fa-chevron-left icon-img"></i>
|
||||
</button>
|
||||
<button className="btn-text" onClick={() => setCurrentDateStamp(currentDateStamp + DAILY_TIMESTAMP)}>
|
||||
<Icon.ChevronRight className="icon-img" />
|
||||
<i className="fa-solid fa-chevron-right icon-img"></i>
|
||||
</button>
|
||||
<button className="btn-text share" onClick={handleShareBtnClick}>
|
||||
<Icon.Share className="icon-img" />
|
||||
<button className="btn-text" onClick={handleShareBtnClick}>
|
||||
<i className="fa-solid fa-share-nodes icon-img"></i>
|
||||
</button>
|
||||
<span className="split-line">/</span>
|
||||
<button className="btn-text" onClick={() => props.destroy()}>
|
||||
<Icon.X className="icon-img" />
|
||||
<i className="fa-solid fa-xmark fa-lg icon-img"></i>
|
||||
</button>
|
||||
</div>
|
||||
<DatePicker
|
||||
@@ -110,7 +108,7 @@ const DailyReviewDialog: React.FC<Props> = (props: Props) => {
|
||||
};
|
||||
|
||||
export default function showDailyReviewDialog(datestamp: DateStamp = Date.now()): void {
|
||||
generateDialog(
|
||||
showDialog(
|
||||
{
|
||||
className: "daily-review-dialog",
|
||||
useAppContext: true,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { ANIMATION_DURATION } from "../helpers/consts";
|
||||
import { Provider } from "react-redux";
|
||||
import { ANIMATION_DURATION } from "../../helpers/consts";
|
||||
import store from "../../store";
|
||||
import "../../less/base-dialog.less";
|
||||
import store from "../store";
|
||||
import "../less/dialog.less";
|
||||
|
||||
interface DialogConfig {
|
||||
className: string;
|
||||
@@ -32,7 +32,7 @@ const BaseDialog: React.FC<Props> = (props: Props) => {
|
||||
);
|
||||
};
|
||||
|
||||
export function generateDialog<T extends DialogProps>(
|
||||
export function showDialog<T extends DialogProps>(
|
||||
config: DialogConfig,
|
||||
DialogComponent: React.FC<T>,
|
||||
props?: Omit<T, "destroy">
|
||||
@@ -1,85 +0,0 @@
|
||||
import Icon from "../Icon";
|
||||
import { generateDialog } from "./BaseDialog";
|
||||
import "../../less/common-dialog.less";
|
||||
|
||||
type DialogStyle = "info" | "warning";
|
||||
|
||||
interface Props extends DialogProps {
|
||||
title: string;
|
||||
content: string;
|
||||
style?: DialogStyle;
|
||||
closeBtnText?: string;
|
||||
confirmBtnText?: string;
|
||||
onClose?: () => void;
|
||||
onConfirm?: () => void;
|
||||
}
|
||||
|
||||
const defaultProps = {
|
||||
title: "",
|
||||
content: "",
|
||||
style: "info",
|
||||
closeBtnText: "Close",
|
||||
confirmBtnText: "Confirm",
|
||||
onClose: () => null,
|
||||
onConfirm: () => null,
|
||||
};
|
||||
|
||||
const CommonDialog: React.FC<Props> = (props: Props) => {
|
||||
const { title, content, destroy, closeBtnText, confirmBtnText, onClose, onConfirm, style } = {
|
||||
...defaultProps,
|
||||
...props,
|
||||
};
|
||||
|
||||
const handleCloseBtnClick = () => {
|
||||
onClose();
|
||||
destroy();
|
||||
};
|
||||
|
||||
const handleConfirmBtnClick = async () => {
|
||||
onConfirm();
|
||||
destroy();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="dialog-header-container">
|
||||
<p className="title-text">{title}</p>
|
||||
<button className="btn close-btn" onClick={handleCloseBtnClick}>
|
||||
<Icon.X />
|
||||
</button>
|
||||
</div>
|
||||
<div className="dialog-content-container">
|
||||
<p className="content-text">{content}</p>
|
||||
<div className="btns-container">
|
||||
<span className="btn cancel-btn" onClick={handleCloseBtnClick}>
|
||||
{closeBtnText}
|
||||
</span>
|
||||
<span className={`btn confirm-btn ${style}`} onClick={handleConfirmBtnClick}>
|
||||
{confirmBtnText}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
interface CommonDialogProps {
|
||||
title: string;
|
||||
content: string;
|
||||
className?: string;
|
||||
style?: DialogStyle;
|
||||
closeBtnText?: string;
|
||||
confirmBtnText?: string;
|
||||
onClose?: () => void;
|
||||
onConfirm?: () => void;
|
||||
}
|
||||
|
||||
export const showCommonDialog = (props: CommonDialogProps) => {
|
||||
generateDialog(
|
||||
{
|
||||
className: `common-dialog ${props?.className ?? ""}`,
|
||||
},
|
||||
CommonDialog,
|
||||
props
|
||||
);
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
export { generateDialog } from "./BaseDialog";
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import * as api from "../helpers/api";
|
||||
import Icon from "./Icon";
|
||||
import "../less/github-badge.less";
|
||||
|
||||
interface Props {}
|
||||
@@ -17,7 +16,7 @@ const GitHubBadge: React.FC<Props> = () => {
|
||||
return (
|
||||
<a className="github-badge-container" href="https://github.com/usememos/memos">
|
||||
<div className="github-icon">
|
||||
<Icon.GitHub className="icon-img" />
|
||||
<i className="fa-brands fa-github fa-lg icon-img"></i>
|
||||
Star
|
||||
</div>
|
||||
<div className="count-text">
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
import * as Icon from "react-feather";
|
||||
|
||||
export default Icon;
|
||||
+24
-11
@@ -1,11 +1,10 @@
|
||||
import { memo, useEffect, useRef, useState } from "react";
|
||||
import { indexOf } from "lodash-es";
|
||||
import { escape, indexOf } from "lodash-es";
|
||||
import dayjs from "dayjs";
|
||||
import relativeTime from "dayjs/plugin/relativeTime";
|
||||
import { IMAGE_URL_REG, UNKNOWN_ID } from "../helpers/consts";
|
||||
import { DONE_BLOCK_REG, formatMemoContent, TODO_BLOCK_REG } from "../helpers/marked";
|
||||
import { IMAGE_URL_REG, LINK_URL_REG, MEMO_LINK_REG, TAG_REG, UNKNOWN_ID } from "../helpers/consts";
|
||||
import { DONE_BLOCK_REG, parseMarkedToHtml, TODO_BLOCK_REG } from "../helpers/marked";
|
||||
import { editorStateService, locationService, memoService, userService } from "../services";
|
||||
import Icon from "./Icon";
|
||||
import Only from "./common/OnlyWhen";
|
||||
import toastHelper from "./Toast";
|
||||
import Image from "./Image";
|
||||
@@ -173,27 +172,30 @@ const Memo: React.FC<Props> = (props: Props) => {
|
||||
<div className="memo-top-wrapper">
|
||||
<div className="status-text-container" onClick={handleShowMemoStoryDialog}>
|
||||
<span className="time-text">{createdAtStr}</span>
|
||||
<Only when={memo.visibility !== "PRIVATE" && !isVisitorMode}>
|
||||
<span className={`status-text ${memo.visibility.toLocaleLowerCase()}`}>{memo.visibility}</span>
|
||||
<Only when={memo.pinned}>
|
||||
<span className="status-text">PINNED</span>
|
||||
</Only>
|
||||
<Only when={memo.visibility === "PUBLIC" && !isVisitorMode}>
|
||||
<span className="status-text">PUBLIC</span>
|
||||
</Only>
|
||||
</div>
|
||||
<div className={`btns-container ${userService.isVisitorMode() ? "!hidden" : ""}`}>
|
||||
<span className="btn more-action-btn">
|
||||
<Icon.MoreHorizontal className="icon-img" />
|
||||
<i className="fa-solid fa-ellipsis icon-img"></i>
|
||||
</span>
|
||||
<div className="more-action-btns-wrapper">
|
||||
<div className="more-action-btns-container">
|
||||
<div className="btns-container">
|
||||
<div className="btn" onClick={handleTogglePinMemoBtnClick}>
|
||||
<Icon.MapPin className={`icon-img ${memo.pinned ? "" : "opacity-20"}`} />
|
||||
<i className={`fa-solid fa-thumbtack icon-img ${memo.pinned ? "" : "opacity-20"}`}></i>
|
||||
<span className="tip-text">{memo.pinned ? "Unpin" : "Pin"}</span>
|
||||
</div>
|
||||
<div className="btn" onClick={handleEditMemoClick}>
|
||||
<Icon.Edit3 className="icon-img" />
|
||||
<i className="fa-solid fa-pen-to-square icon-img"></i>
|
||||
<span className="tip-text">Edit</span>
|
||||
</div>
|
||||
<div className="btn" onClick={handleGenMemoImageBtnClick}>
|
||||
<Icon.Share className="icon-img" />
|
||||
<i className="fa-solid fa-share-nodes icon-img"></i>
|
||||
<span className="tip-text">Share</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -220,7 +222,7 @@ const Memo: React.FC<Props> = (props: Props) => {
|
||||
<div className="expand-btn-container">
|
||||
<span className={`btn ${state.expandButtonStatus === 0 ? "expand-btn" : "fold-btn"}`} onClick={handleExpandBtnClick}>
|
||||
{state.expandButtonStatus === 0 ? "Expand" : "Fold"}
|
||||
<Icon.ChevronRight className="icon-img" />
|
||||
<i className="fa-solid fa-chevron-right icon-img"></i>
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
@@ -235,4 +237,15 @@ const Memo: React.FC<Props> = (props: Props) => {
|
||||
);
|
||||
};
|
||||
|
||||
export function formatMemoContent(content: string) {
|
||||
const tempElement = document.createElement("div");
|
||||
tempElement.innerHTML = parseMarkedToHtml(escape(content));
|
||||
|
||||
return tempElement.innerHTML
|
||||
.replace(IMAGE_URL_REG, "")
|
||||
.replace(MEMO_LINK_REG, "<span class='memo-link-text' data-value='$2'>$1</span>")
|
||||
.replace(LINK_URL_REG, "<a class='link' target='_blank' rel='noreferrer' href='$2'>$1</a>")
|
||||
.replace(TAG_REG, "<span class='tag-span'>#$1</span> ");
|
||||
}
|
||||
|
||||
export default memo(Memo);
|
||||
|
||||
@@ -2,14 +2,13 @@ import { useState, useEffect, useCallback } from "react";
|
||||
import { editorStateService, memoService, userService } from "../services";
|
||||
import { IMAGE_URL_REG, MEMO_LINK_REG, UNKNOWN_ID } from "../helpers/consts";
|
||||
import * as utils from "../helpers/utils";
|
||||
import { formatMemoContent, parseHtmlToRawText } from "../helpers/marked";
|
||||
import { parseHtmlToRawText } from "../helpers/marked";
|
||||
import Only from "./common/OnlyWhen";
|
||||
import toastHelper from "./Toast";
|
||||
import { generateDialog } from "./Dialog";
|
||||
import { showDialog } from "./Dialog";
|
||||
import Image from "./Image";
|
||||
import { formatMemoContent } from "./Memo";
|
||||
import "../less/memo-card-dialog.less";
|
||||
import Selector from "./common/Selector";
|
||||
import Icon from "./Icon";
|
||||
|
||||
interface LinkedMemo extends Memo {
|
||||
createdAtStr: string;
|
||||
@@ -27,11 +26,6 @@ const MemoCardDialog: React.FC<Props> = (props: Props) => {
|
||||
const [linkMemos, setLinkMemos] = useState<LinkedMemo[]>([]);
|
||||
const [linkedMemos, setLinkedMemos] = useState<LinkedMemo[]>([]);
|
||||
const imageUrls = Array.from(memo.content.match(IMAGE_URL_REG) ?? []).map((s) => s.replace(IMAGE_URL_REG, "$1"));
|
||||
const visibilityList = [
|
||||
{ text: "PUBLIC", value: "PUBLIC" },
|
||||
{ text: "PROTECTED", value: "PROTECTED" },
|
||||
{ text: "PRIVATE", value: "PRIVATE" },
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
const fetchLinkedMemos = async () => {
|
||||
@@ -104,16 +98,25 @@ const MemoCardDialog: React.FC<Props> = (props: Props) => {
|
||||
setMemo(memo);
|
||||
}, []);
|
||||
|
||||
const handleEditMemoBtnClick = () => {
|
||||
const handleEditMemoBtnClick = useCallback(() => {
|
||||
props.destroy();
|
||||
editorStateService.setEditMemoWithId(memo.id);
|
||||
}, [memo.id]);
|
||||
|
||||
const handlePinClick = async () => {
|
||||
if (memo.pinned) {
|
||||
await memoService.unpinMemo(memo.id);
|
||||
} else {
|
||||
await memoService.pinMemo(memo.id);
|
||||
}
|
||||
setMemo({
|
||||
...memo,
|
||||
pinned: !memo.pinned,
|
||||
});
|
||||
};
|
||||
|
||||
const handleVisibilitySelectorChange = async (visibility: Visibility) => {
|
||||
if (memo.visibility === visibility) {
|
||||
return;
|
||||
}
|
||||
|
||||
const handleVisibilityClick = async () => {
|
||||
const visibility = memo.visibility === "PRIVATE" ? "PUBLIC" : "PRIVATE";
|
||||
await memoService.patchMemo({
|
||||
id: memo.id,
|
||||
visibility: visibility,
|
||||
@@ -126,33 +129,25 @@ const MemoCardDialog: React.FC<Props> = (props: Props) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Only when={!userService.isVisitorMode()}>
|
||||
<div className="card-header-container">
|
||||
<div className="visibility-selector-container">
|
||||
<Icon.Eye className="icon-img" />
|
||||
<Selector
|
||||
className="visibility-selector"
|
||||
dataSource={visibilityList}
|
||||
value={memo.visibility}
|
||||
handleValueChanged={(value) => handleVisibilitySelectorChange(value as Visibility)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Only>
|
||||
<div className="memo-card-container">
|
||||
<div className="header-container">
|
||||
<p className="time-text">{utils.getDateTimeString(memo.createdTs)}</p>
|
||||
<div className="btns-container">
|
||||
<Only when={!userService.isVisitorMode()}>
|
||||
<>
|
||||
<button className="btn edit-btn" onClick={handleEditMemoBtnClick}>
|
||||
<Icon.Edit3 className="icon-img" />
|
||||
<button className="btn" onClick={handlePinClick}>
|
||||
<i className={`fa-solid fa-thumbtack icon-img ${memo.pinned ? "" : "opacity-20"}`}></i>
|
||||
</button>
|
||||
<button className="btn" onClick={handleVisibilityClick}>
|
||||
<i className={`fa-solid fa-eye icon-img ${memo.visibility === "PUBLIC" ? "" : "opacity-20"}`}></i>
|
||||
</button>
|
||||
<button className="btn edit-btn" onClick={handleEditMemoBtnClick}>
|
||||
<i className="fa-solid fa-pen-to-square icon-img"></i>
|
||||
</button>
|
||||
<span className="split-line">/</span>
|
||||
</>
|
||||
</Only>
|
||||
<button className="btn close-btn" onClick={props.destroy}>
|
||||
<Icon.X className="icon-img" />
|
||||
<i className="fa-solid fa-xmark fa-lg icon-img"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -223,7 +218,7 @@ const MemoCardDialog: React.FC<Props> = (props: Props) => {
|
||||
};
|
||||
|
||||
export default function showMemoCardDialog(memo: Memo): void {
|
||||
generateDialog(
|
||||
showDialog(
|
||||
{
|
||||
className: "memo-card-dialog",
|
||||
},
|
||||
|
||||
@@ -3,7 +3,6 @@ import { UNKNOWN_ID } from "../helpers/consts";
|
||||
import { editorStateService, locationService, memoService, resourceService } from "../services";
|
||||
import { useAppSelector } from "../store";
|
||||
import * as storage from "../helpers/storage";
|
||||
import Icon from "./Icon";
|
||||
import toastHelper from "./Toast";
|
||||
import Editor, { EditorRefActions } from "./Editor/Editor";
|
||||
import "../less/memo-editor.less";
|
||||
@@ -108,16 +107,16 @@ const MemoEditor: React.FC<Props> = () => {
|
||||
const { type } = file;
|
||||
|
||||
if (!type.startsWith("image")) {
|
||||
toastHelper.error("Only image file supported.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const image = await resourceService.upload(file);
|
||||
const url = `/h/r/${image.id}/${image.filename}`;
|
||||
|
||||
return url;
|
||||
} catch (error: any) {
|
||||
toastHelper.error("Failed to upload image\n" + JSON.stringify(error, null, 4));
|
||||
toastHelper.error(error);
|
||||
} finally {
|
||||
setState({
|
||||
...state,
|
||||
@@ -232,7 +231,7 @@ const MemoEditor: React.FC<Props> = () => {
|
||||
tools={
|
||||
<>
|
||||
<div className="action-btn tag-action">
|
||||
<Icon.Hash className="icon-img" />
|
||||
<i className="fa-solid fa-hashtag icon-img"></i>
|
||||
<div ref={tagSeletorRef} className="tag-list" onClick={handleTagSeletorClick}>
|
||||
{tags.map((t) => {
|
||||
return <span key={t}>{t}</span>;
|
||||
@@ -240,11 +239,11 @@ const MemoEditor: React.FC<Props> = () => {
|
||||
</div>
|
||||
</div>
|
||||
<button className="action-btn">
|
||||
<Icon.Image className="icon-img" onClick={handleUploadFileBtnClick} />
|
||||
<i className="fa-solid fa-image icon-img" onClick={handleUploadFileBtnClick}></i>
|
||||
<span className={`tip-text ${state.isUploadingResource ? "!block" : ""}`}>Uploading</span>
|
||||
</button>
|
||||
<button className="action-btn" onClick={handleFullscreenBtnClick}>
|
||||
{state.fullscreen ? <Icon.Minimize className="icon-img" /> : <Icon.Maximize className="icon-img" />}
|
||||
<i className={`fa-solid fa-${state.fullscreen ? "compress" : "expand"} icon-img`}></i>
|
||||
</button>
|
||||
</>
|
||||
}
|
||||
|
||||
@@ -83,6 +83,7 @@ const MemoList: React.FC<Props> = () => {
|
||||
.fetchAllMemos()
|
||||
.then(() => {
|
||||
setFetchStatus(false);
|
||||
memoService.updateTagsState();
|
||||
})
|
||||
.catch(() => {
|
||||
toastHelper.error("😭 Fetching failed, please try again later.");
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { memoService, shortcutService } from "../services";
|
||||
import { useAppSelector } from "../store";
|
||||
import Icon from "./Icon";
|
||||
import SearchBar from "./SearchBar";
|
||||
import { toggleSiderbar } from "./Sidebar";
|
||||
import "../less/memos-header.less";
|
||||
@@ -41,7 +40,7 @@ const MemosHeader: React.FC<Props> = () => {
|
||||
<div className="section-header-container memos-header-container">
|
||||
<div className="title-container">
|
||||
<div className="action-btn" onClick={toggleSiderbar}>
|
||||
<Icon.Menu className="icon-img" />
|
||||
<i className="fa-solid fa-bars icon-img"></i>
|
||||
</div>
|
||||
<span className="title-text" onClick={handleTitleTextClick}>
|
||||
{titleText}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { showDialog } from "./Dialog";
|
||||
import * as utils from "../helpers/utils";
|
||||
import Icon from "./Icon";
|
||||
import { generateDialog } from "./Dialog";
|
||||
import "../less/preview-image-dialog.less";
|
||||
|
||||
interface Props extends DialogProps {
|
||||
@@ -23,10 +22,10 @@ const PreviewImageDialog: React.FC<Props> = ({ destroy, imgUrl }: Props) => {
|
||||
<>
|
||||
<div className="btns-container">
|
||||
<button className="btn" onClick={handleCloseBtnClick}>
|
||||
<Icon.X className="icon-img" />
|
||||
<i className="fa-solid fa-xmark fa-lg icon-img"></i>
|
||||
</button>
|
||||
<button className="btn" onClick={handleDownloadBtnClick}>
|
||||
<Icon.Download className="icon-img" />
|
||||
<i className="fa-solid fa-download icon-img"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div className="img-container">
|
||||
@@ -37,7 +36,7 @@ const PreviewImageDialog: React.FC<Props> = ({ destroy, imgUrl }: Props) => {
|
||||
};
|
||||
|
||||
export default function showPreviewImageDialog(imgUrl: string): void {
|
||||
generateDialog(
|
||||
showDialog(
|
||||
{
|
||||
className: "preview-image-dialog",
|
||||
},
|
||||
|
||||
@@ -1,163 +0,0 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import * as utils from "../helpers/utils";
|
||||
import useLoading from "../hooks/useLoading";
|
||||
import { resourceService } from "../services";
|
||||
import Dropdown from "./common/Dropdown";
|
||||
import { generateDialog } from "./Dialog";
|
||||
import { showCommonDialog } from "./Dialog/CommonDialog";
|
||||
import toastHelper from "./Toast";
|
||||
import Icon from "./Icon";
|
||||
import "../less/resources-dialog.less";
|
||||
|
||||
interface Props extends DialogProps {}
|
||||
|
||||
interface State {
|
||||
resources: Resource[];
|
||||
isUploadingResource: boolean;
|
||||
}
|
||||
|
||||
const ResourcesDialog: React.FC<Props> = (props: Props) => {
|
||||
const { destroy } = props;
|
||||
const loadingState = useLoading();
|
||||
const [state, setState] = useState<State>({
|
||||
resources: [],
|
||||
isUploadingResource: false,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
fetchResources()
|
||||
.catch((error) => {
|
||||
toastHelper.error("Failed to fetch archived memos: ", error);
|
||||
})
|
||||
.finally(() => {
|
||||
loadingState.setFinish();
|
||||
});
|
||||
}, []);
|
||||
|
||||
const fetchResources = async () => {
|
||||
const data = await resourceService.getResourceList();
|
||||
setState({
|
||||
...state,
|
||||
resources: data,
|
||||
});
|
||||
};
|
||||
|
||||
const handleUploadFileBtnClick = async () => {
|
||||
if (state.isUploadingResource) {
|
||||
return;
|
||||
}
|
||||
|
||||
const inputEl = document.createElement("input");
|
||||
inputEl.type = "file";
|
||||
inputEl.multiple = false;
|
||||
inputEl.accept = "image/png, image/gif, image/jpeg";
|
||||
inputEl.onchange = async () => {
|
||||
if (!inputEl.files || inputEl.files.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
setState({
|
||||
...state,
|
||||
isUploadingResource: true,
|
||||
});
|
||||
|
||||
const file = inputEl.files[0];
|
||||
try {
|
||||
await resourceService.upload(file);
|
||||
} catch (error: any) {
|
||||
toastHelper.error("Failed to upload resource\n" + JSON.stringify(error, null, 4));
|
||||
} finally {
|
||||
setState({
|
||||
...state,
|
||||
isUploadingResource: false,
|
||||
});
|
||||
await fetchResources();
|
||||
}
|
||||
};
|
||||
inputEl.click();
|
||||
};
|
||||
|
||||
const handleCopyResourceLinkBtnClick = (resource: Resource) => {
|
||||
utils.copyTextToClipboard(`${window.location.origin}/h/r/${resource.id}/${resource.filename}`);
|
||||
toastHelper.success("Succeed to copy resource link to clipboard");
|
||||
};
|
||||
|
||||
const handleDeleteResourceBtnClick = (resource: Resource) => {
|
||||
showCommonDialog({
|
||||
title: `Delete Resource`,
|
||||
content: `Are you sure to delete this resource? THIS ACTION IS IRREVERSIABLE.❗️`,
|
||||
style: "warning",
|
||||
onConfirm: async () => {
|
||||
await resourceService.deleteResourceById(resource.id);
|
||||
await fetchResources();
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="dialog-header-container">
|
||||
<p className="title-text">
|
||||
<span className="icon-text">🌄</span>
|
||||
Resources
|
||||
</p>
|
||||
<button className="btn close-btn" onClick={destroy}>
|
||||
<Icon.X className="icon-img" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="dialog-content-container">
|
||||
<div className="tip-text-container">(👨💻WIP) View your static resources in memos. e.g. images</div>
|
||||
<div className="upload-resource-container" onClick={() => handleUploadFileBtnClick()}>
|
||||
<div className="upload-resource-btn">
|
||||
<Icon.File className="icon-img" />
|
||||
<span>Upload</span>
|
||||
</div>
|
||||
</div>
|
||||
{loadingState.isLoading ? (
|
||||
<div className="loading-text-container">
|
||||
<p className="tip-text">fetching data...</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="resource-table-container">
|
||||
<div className="fields-container">
|
||||
<span className="field-text">ID</span>
|
||||
<span className="field-text name-text">NAME</span>
|
||||
<span className="field-text">TYPE</span>
|
||||
<span></span>
|
||||
</div>
|
||||
{state.resources.length === 0 ? (
|
||||
<p className="tip-text">No resource.</p>
|
||||
) : (
|
||||
state.resources.map((resource) => (
|
||||
<div key={resource.id} className="resource-container">
|
||||
<span className="field-text">{resource.id}</span>
|
||||
<span className="field-text name-text">{resource.filename}</span>
|
||||
<span className="field-text">{resource.type}</span>
|
||||
<div className="buttons-container">
|
||||
<Dropdown className="actions-dropdown">
|
||||
<button onClick={() => handleCopyResourceLinkBtnClick(resource)}>Copy Link</button>
|
||||
<button className="delete-btn" onClick={() => handleDeleteResourceBtnClick(resource)}>
|
||||
Delete
|
||||
</button>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default function showResourcesDialog() {
|
||||
generateDialog(
|
||||
{
|
||||
className: "resources-dialog",
|
||||
useAppContext: true,
|
||||
},
|
||||
ResourcesDialog,
|
||||
{}
|
||||
);
|
||||
}
|
||||
@@ -2,7 +2,6 @@ import { locationService } from "../services";
|
||||
import { useAppSelector } from "../store";
|
||||
import { memoSpecialTypes } from "../helpers/filter";
|
||||
import "../less/search-bar.less";
|
||||
import Icon from "./Icon";
|
||||
|
||||
interface Props {}
|
||||
|
||||
@@ -25,7 +24,7 @@ const SearchBar: React.FC<Props> = () => {
|
||||
return (
|
||||
<div className="search-bar-container">
|
||||
<div className="search-bar-inputer">
|
||||
<Icon.Search className="icon-img" />
|
||||
<i className="fa-solid fa-magnifying-glass fa-sm icon-img"></i>
|
||||
<input className="text-input" type="text" placeholder="" onChange={handleTextQueryInput} />
|
||||
</div>
|
||||
<div className="quickly-action-wrapper">
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { useState } from "react";
|
||||
import { useAppSelector } from "../store";
|
||||
import { generateDialog } from "./Dialog";
|
||||
import { showDialog } from "./Dialog";
|
||||
import MyAccountSection from "./Settings/MyAccountSection";
|
||||
import PreferencesSection from "./Settings/PreferencesSection";
|
||||
import MemberSection from "./Settings/MemberSection";
|
||||
import "../less/setting-dialog.less";
|
||||
import Icon from "./Icon";
|
||||
|
||||
interface Props extends DialogProps {}
|
||||
|
||||
@@ -31,7 +30,7 @@ const SettingDialog: React.FC<Props> = (props: Props) => {
|
||||
return (
|
||||
<div className="dialog-content-container">
|
||||
<button className="btn close-btn" onClick={destroy}>
|
||||
<Icon.X className="icon-img" />
|
||||
<i className="fa-solid fa-xmark fa-lg icon-img"></i>
|
||||
</button>
|
||||
<div className="section-selector-container">
|
||||
<span className="section-title">Basic</span>
|
||||
@@ -77,7 +76,7 @@ const SettingDialog: React.FC<Props> = (props: Props) => {
|
||||
};
|
||||
|
||||
export default function showSettingDialog(): void {
|
||||
generateDialog(
|
||||
showDialog(
|
||||
{
|
||||
className: "setting-dialog",
|
||||
useAppContext: true,
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { isEmpty } from "lodash-es";
|
||||
import { userService } from "../../services";
|
||||
import { useAppSelector } from "../../store";
|
||||
import * as api from "../../helpers/api";
|
||||
import toastHelper from "../Toast";
|
||||
import Dropdown from "../common/Dropdown";
|
||||
import { showCommonDialog } from "../Dialog/CommonDialog";
|
||||
import "../../less/settings/member-section.less";
|
||||
|
||||
interface Props {}
|
||||
@@ -16,7 +12,6 @@ interface State {
|
||||
}
|
||||
|
||||
const PreferencesSection: React.FC<Props> = () => {
|
||||
const currentUser = useAppSelector((state) => state.user.user);
|
||||
const [state, setState] = useState<State>({
|
||||
createUserEmail: "",
|
||||
createUserPassword: "",
|
||||
@@ -71,43 +66,6 @@ const PreferencesSection: React.FC<Props> = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const handleArchiveUserClick = (user: User) => {
|
||||
showCommonDialog({
|
||||
title: `Archive Member`,
|
||||
content: `❗️Are you sure to archive ${user.name}?`,
|
||||
style: "warning",
|
||||
onConfirm: async () => {
|
||||
await userService.patchUser({
|
||||
id: user.id,
|
||||
rowStatus: "ARCHIVED",
|
||||
});
|
||||
fetchUserList();
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleRestoreUserClick = async (user: User) => {
|
||||
await userService.patchUser({
|
||||
id: user.id,
|
||||
rowStatus: "NORMAL",
|
||||
});
|
||||
fetchUserList();
|
||||
};
|
||||
|
||||
const handleDeleteUserClick = (user: User) => {
|
||||
showCommonDialog({
|
||||
title: `Delete Member`,
|
||||
content: `Are you sure to delete ${user.name}? THIS ACTION IS IRREVERSIABLE.❗️`,
|
||||
style: "warning",
|
||||
onConfirm: async () => {
|
||||
await userService.deleteUser({
|
||||
id: user.id,
|
||||
});
|
||||
fetchUserList();
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="section-container member-section-container">
|
||||
<p className="title-text">Create a member</p>
|
||||
@@ -128,30 +86,15 @@ const PreferencesSection: React.FC<Props> = () => {
|
||||
<div className="member-container field-container">
|
||||
<span className="field-text">ID</span>
|
||||
<span className="field-text">EMAIL</span>
|
||||
<span></span>
|
||||
</div>
|
||||
{userList.map((user) => (
|
||||
<div key={user.id} className={`member-container ${user.rowStatus === "ARCHIVED" ? "archived" : ""}`}>
|
||||
<div key={user.id} className="member-container">
|
||||
<span className="field-text id-text">{user.id}</span>
|
||||
<span className="field-text email-text">{user.email}</span>
|
||||
<div className="buttons-container">
|
||||
{currentUser?.id === user.id ? (
|
||||
<span className="tip-text">Yourself</span>
|
||||
) : (
|
||||
<Dropdown className="actions-dropdown">
|
||||
{user.rowStatus === "NORMAL" ? (
|
||||
<button onClick={() => handleArchiveUserClick(user)}>Archive</button>
|
||||
) : (
|
||||
<>
|
||||
<button onClick={() => handleRestoreUserClick(user)}>Restore</button>
|
||||
<button className="delete" onClick={() => handleDeleteUserClick(user)}>
|
||||
Delete
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</Dropdown>
|
||||
)}
|
||||
</div>
|
||||
{/* TODO */}
|
||||
{/* <div className="buttons-container">
|
||||
<span>delete</span>
|
||||
</div> */}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -3,8 +3,8 @@ import { useAppSelector } from "../../store";
|
||||
import { userService } from "../../services";
|
||||
import { validate, ValidatorConfig } from "../../helpers/validator";
|
||||
import toastHelper from "../Toast";
|
||||
import { showCommonDialog } from "../Dialog/CommonDialog";
|
||||
import showChangePasswordDialog from "../ChangePasswordDialog";
|
||||
import showConfirmResetOpenIdDialog from "../ConfirmResetOpenIdDialog";
|
||||
import "../../less/settings/my-account-section.less";
|
||||
|
||||
const validateConfig: ValidatorConfig = {
|
||||
@@ -39,7 +39,6 @@ const MyAccountSection: React.FC<Props> = () => {
|
||||
|
||||
try {
|
||||
await userService.patchUser({
|
||||
id: user.id,
|
||||
name: username,
|
||||
});
|
||||
toastHelper.info("Username changed");
|
||||
@@ -53,11 +52,7 @@ const MyAccountSection: React.FC<Props> = () => {
|
||||
};
|
||||
|
||||
const handleResetOpenIdBtnClick = async () => {
|
||||
showCommonDialog({
|
||||
title: "Reset Open API",
|
||||
content: "❗️The existing API will be invalidated and a new one will be generated, are you sure you want to reset?",
|
||||
style: "warning",
|
||||
});
|
||||
showConfirmResetOpenIdDialog();
|
||||
};
|
||||
|
||||
const handlePreventDefault = (e: React.MouseEvent) => {
|
||||
|
||||
@@ -3,11 +3,10 @@ import { userService } from "../services";
|
||||
import toImage from "../labs/html2image";
|
||||
import { ANIMATION_DURATION, IMAGE_URL_REG } from "../helpers/consts";
|
||||
import * as utils from "../helpers/utils";
|
||||
import { formatMemoContent } from "../helpers/marked";
|
||||
import Icon from "./Icon";
|
||||
import { generateDialog } from "./Dialog";
|
||||
import { showDialog } from "./Dialog";
|
||||
import Only from "./common/OnlyWhen";
|
||||
import toastHelper from "./Toast";
|
||||
import { formatMemoContent } from "./Memo";
|
||||
import "../less/share-memo-image-dialog.less";
|
||||
|
||||
interface Props extends DialogProps {
|
||||
@@ -76,7 +75,7 @@ const ShareMemoImageDialog: React.FC<Props> = (props: Props) => {
|
||||
<span className="icon-text">🌄</span>Share Memo
|
||||
</p>
|
||||
<button className="btn close-btn" onClick={handleCloseBtnClick}>
|
||||
<Icon.X className="icon-img" />
|
||||
<i className="fa-solid fa-xmark fa-lg icon-img"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div className="dialog-content-container">
|
||||
@@ -108,7 +107,7 @@ const ShareMemoImageDialog: React.FC<Props> = (props: Props) => {
|
||||
};
|
||||
|
||||
export default function showShareMemoImageDialog(memo: Memo): void {
|
||||
generateDialog(
|
||||
showDialog(
|
||||
{
|
||||
className: "share-memo-image-dialog",
|
||||
},
|
||||
|
||||
@@ -4,7 +4,6 @@ import { useAppSelector } from "../store";
|
||||
import * as utils from "../helpers/utils";
|
||||
import useToggle from "../hooks/useToggle";
|
||||
import useLoading from "../hooks/useLoading";
|
||||
import Icon from "./Icon";
|
||||
import toastHelper from "./Toast";
|
||||
import showCreateShortcutDialog from "./CreateShortcutDialog";
|
||||
import "../less/shortcut-list.less";
|
||||
@@ -40,7 +39,7 @@ const ShortcutList: React.FC<Props> = () => {
|
||||
<p className="title-text">
|
||||
<span className="normal-text">Shortcuts</span>
|
||||
<button className="btn" onClick={() => showCreateShortcutDialog()}>
|
||||
<Icon.Plus className="icon-img" />
|
||||
<i className="fa-solid fa-plus icon-img fa-xs"></i>
|
||||
</button>
|
||||
</p>
|
||||
<div className="shortcuts-container">
|
||||
@@ -114,7 +113,7 @@ const ShortcutContainer: React.FC<ShortcutContainerProps> = (props: ShortcutCont
|
||||
</div>
|
||||
<div className="btns-container">
|
||||
<span className="action-btn toggle-btn">
|
||||
<Icon.MoreHorizontal className="icon-img" />
|
||||
<i className="fa-solid fa-ellipsis fa-sm icon-img"></i>
|
||||
</span>
|
||||
<div className="action-btns-wrapper">
|
||||
<div className="action-btns-container">
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { userService } from "../services";
|
||||
import Icon from "./Icon";
|
||||
import { useAppSelector } from "../store";
|
||||
import Only from "./common/OnlyWhen";
|
||||
import showDailyReviewDialog from "./DailyReviewDialog";
|
||||
import showSettingDialog from "./SettingDialog";
|
||||
import showArchivedMemoDialog from "./ArchivedMemoDialog";
|
||||
import showResourcesDialog from "./ResourcesDialog";
|
||||
import UserBanner from "./UserBanner";
|
||||
import UsageHeatMap from "./UsageHeatMap";
|
||||
import ShortcutList from "./ShortcutList";
|
||||
@@ -14,14 +13,12 @@ import "../less/siderbar.less";
|
||||
interface Props {}
|
||||
|
||||
const Sidebar: React.FC<Props> = () => {
|
||||
const user = useAppSelector((state) => state.user.user);
|
||||
|
||||
const handleMyAccountBtnClick = () => {
|
||||
showSettingDialog();
|
||||
};
|
||||
|
||||
const handleResourcesBtnClick = () => {
|
||||
showResourcesDialog();
|
||||
};
|
||||
|
||||
const handleArchivedBtnClick = () => {
|
||||
showArchivedMemoDialog();
|
||||
};
|
||||
@@ -30,7 +27,7 @@ const Sidebar: React.FC<Props> = () => {
|
||||
<aside className="sidebar-wrapper">
|
||||
<div className="close-container">
|
||||
<span className="action-btn" onClick={toggleSiderbar}>
|
||||
<Icon.X className="icon-img" />
|
||||
<i className="fa-solid fa-xmark fa-lg icon-img"></i>
|
||||
</span>
|
||||
</div>
|
||||
<UserBanner />
|
||||
@@ -40,9 +37,6 @@ const Sidebar: React.FC<Props> = () => {
|
||||
<span className="icon">📅</span> Daily Review
|
||||
</button>
|
||||
<Only when={!userService.isVisitorMode()}>
|
||||
<button className="btn action-btn" onClick={handleResourcesBtnClick}>
|
||||
<span className="icon">🌄</span> Resources
|
||||
</button>
|
||||
<button className="btn action-btn" onClick={handleMyAccountBtnClick}>
|
||||
<span className="icon">⚙️</span> Setting
|
||||
</button>
|
||||
@@ -50,6 +44,17 @@ const Sidebar: React.FC<Props> = () => {
|
||||
<button className="btn action-btn" onClick={handleArchivedBtnClick}>
|
||||
<span className="icon">🗂</span> Archived
|
||||
</button>
|
||||
<Only when={userService.isVisitorMode()}>
|
||||
{user ? (
|
||||
<button className="btn action-btn" onClick={() => (window.location.href = "/")}>
|
||||
<span className="icon">🏠</span> Back to Home
|
||||
</button>
|
||||
) : (
|
||||
<button className="btn action-btn" onClick={() => (window.location.href = "/signin")}>
|
||||
<span className="icon">👉</span> Sign in
|
||||
</button>
|
||||
)}
|
||||
</Only>
|
||||
</div>
|
||||
<Only when={!userService.isVisitorMode()}>
|
||||
<ShortcutList />
|
||||
|
||||
@@ -3,7 +3,6 @@ import * as utils from "../helpers/utils";
|
||||
import { useAppSelector } from "../store";
|
||||
import { locationService, memoService, userService } from "../services";
|
||||
import useToggle from "../hooks/useToggle";
|
||||
import Icon from "./Icon";
|
||||
import Only from "./common/OnlyWhen";
|
||||
import "../less/tag-list.less";
|
||||
|
||||
@@ -21,9 +20,7 @@ const TagList: React.FC<Props> = () => {
|
||||
const [tags, setTags] = useState<Tag[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (memos.length > 0) {
|
||||
memoService.updateTagsState();
|
||||
}
|
||||
memoService.updateTagsState();
|
||||
}, [memos]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -119,7 +116,7 @@ const TagItemContainer: React.FC<TagItemContainerProps> = (props: TagItemContain
|
||||
<div className="btns-container">
|
||||
{hasSubTags ? (
|
||||
<span className={`action-btn toggle-btn ${showSubTags ? "shown" : ""}`} onClick={handleToggleBtnClick}>
|
||||
<Icon.ChevronRight className="icon-img" />
|
||||
<i className="fa-solid fa-chevron-right icon-img"></i>
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,6 @@ import * as utils from "../helpers/utils";
|
||||
import userService from "../services/userService";
|
||||
import { locationService } from "../services";
|
||||
import { useAppSelector } from "../store";
|
||||
import Icon from "./Icon";
|
||||
import MenuBtnsPopup from "./MenuBtnsPopup";
|
||||
import "../less/user-banner.less";
|
||||
|
||||
@@ -46,7 +45,7 @@ const UserBanner: React.FC<Props> = () => {
|
||||
{!isVisitorMode && user?.role === "HOST" ? <span className="tag">MOD</span> : null}
|
||||
</div>
|
||||
<button className="action-btn menu-popup-btn" onClick={handlePopupBtnClick}>
|
||||
<Icon.MoreHorizontal className="icon-img" />
|
||||
<i className="fa-solid fa-ellipsis icon-img"></i>
|
||||
</button>
|
||||
<MenuBtnsPopup shownStatus={shouldShowPopupBtns} setShownStatus={setShouldShowPopupBtns} />
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { DAILY_TIMESTAMP } from "../../helpers/consts";
|
||||
import "../../less/common/date-picker.less";
|
||||
import Icon from "../Icon";
|
||||
|
||||
interface DatePickerProps {
|
||||
className?: string;
|
||||
@@ -56,13 +55,13 @@ const DatePicker: React.FC<DatePickerProps> = (props: DatePickerProps) => {
|
||||
<div className={`date-picker-wrapper ${className}`}>
|
||||
<div className="date-picker-header">
|
||||
<span className="btn-text" onClick={() => handleChangeMonthBtnClick(-1)}>
|
||||
<Icon.ChevronLeft className="icon-img" />
|
||||
<i className="fa-solid fa-chevron-left icon-img"></i>
|
||||
</span>
|
||||
<span className="normal-text">
|
||||
{firstDate.getFullYear()}/{firstDate.getMonth() + 1}
|
||||
</span>
|
||||
<span className="btn-text" onClick={() => handleChangeMonthBtnClick(1)}>
|
||||
<Icon.ChevronRight className="icon-img" />
|
||||
<i className="fa-solid fa-chevron-right icon-img"></i>
|
||||
</span>
|
||||
</div>
|
||||
<div className="date-picker-day-container">
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
import { ReactNode, useEffect, useRef } from "react";
|
||||
import useToggle from "../../hooks/useToggle";
|
||||
import Icon from "../Icon";
|
||||
import "../../less/common/dropdown.less";
|
||||
|
||||
interface DropdownProps {
|
||||
children?: ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const Dropdown: React.FC<DropdownProps> = (props: DropdownProps) => {
|
||||
const { children, className } = props;
|
||||
const [dropdownStatus, toggleDropdownStatus] = useToggle(false);
|
||||
const dropdownWrapperRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (dropdownStatus) {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (!dropdownWrapperRef.current?.contains(event.target as Node)) {
|
||||
toggleDropdownStatus(false);
|
||||
}
|
||||
};
|
||||
window.addEventListener("click", handleClickOutside, {
|
||||
capture: true,
|
||||
once: true,
|
||||
});
|
||||
}
|
||||
}, [dropdownStatus]);
|
||||
|
||||
return (
|
||||
<div ref={dropdownWrapperRef} className={`dropdown-wrapper ${className ?? ""}`} onClick={() => toggleDropdownStatus()}>
|
||||
<span className="trigger-button">
|
||||
<Icon.MoreHorizontal className="icon-img" />
|
||||
</span>
|
||||
<div className={`action-buttons-container ${dropdownStatus ? "" : "!hidden"}`}>{children}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Dropdown;
|
||||
@@ -1,6 +1,5 @@
|
||||
import { memo, useEffect, useRef } from "react";
|
||||
import useToggle from "../../hooks/useToggle";
|
||||
import Icon from "../Icon";
|
||||
import "../../less/common/selector.less";
|
||||
|
||||
interface TVObject {
|
||||
@@ -65,7 +64,7 @@ const Selector: React.FC<Props> = (props: Props) => {
|
||||
<div className={`current-value-container ${showSelector ? "active" : ""}`} onClick={handleCurrentValueClick}>
|
||||
<span className="value-text">{currentItem.text}</span>
|
||||
<span className="arrow-text">
|
||||
<Icon.ChevronDown className="icon-img" />
|
||||
<i className="fa-solid fa-chevron-down fa-sm icon-img"></i>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
||||
+2
-14
@@ -34,7 +34,7 @@ export function createUser(userCreate: UserCreate) {
|
||||
return axios.post<ResponseObject<User>>("/api/user", userCreate);
|
||||
}
|
||||
|
||||
export function getMyselfUser() {
|
||||
export function getUser() {
|
||||
return axios.get<ResponseObject<User>>("/api/user/me");
|
||||
}
|
||||
|
||||
@@ -47,11 +47,7 @@ export function getUserById(id: number) {
|
||||
}
|
||||
|
||||
export function patchUser(userPatch: UserPatch) {
|
||||
return axios.patch<ResponseObject<User>>(`/api/user/${userPatch.id}`, userPatch);
|
||||
}
|
||||
|
||||
export function deleteUser(userDelete: UserDelete) {
|
||||
return axios.delete(`/api/user/${userDelete.id}`);
|
||||
return axios.patch<ResponseObject<User>>("/api/user/me", userPatch);
|
||||
}
|
||||
|
||||
export function getMemoList(memoFind?: MemoFind) {
|
||||
@@ -109,18 +105,10 @@ export function deleteShortcutById(shortcutId: ShortcutId) {
|
||||
return axios.delete(`/api/shortcut/${shortcutId}`);
|
||||
}
|
||||
|
||||
export function getResourceList() {
|
||||
return axios.get<ResponseObject<Resource[]>>("/api/resource");
|
||||
}
|
||||
|
||||
export function uploadFile(formData: FormData) {
|
||||
return axios.post<ResponseObject<Resource>>("/api/resource", formData);
|
||||
}
|
||||
|
||||
export function deleteResourceById(id: ResourceId) {
|
||||
return axios.delete(`/api/resource/${id}`);
|
||||
}
|
||||
|
||||
export function getTagList(tagFind?: TagFind) {
|
||||
const queryList = [];
|
||||
if (tagFind?.creatorId) {
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
import { escape } from "lodash-es";
|
||||
import { IMAGE_URL_REG, LINK_URL_REG, MEMO_LINK_REG, TAG_REG } from "./consts";
|
||||
|
||||
const CODE_BLOCK_REG = /```([\s\S]*?)```/g;
|
||||
const BOLD_TEXT_REG = /\*\*(.+?)\*\*/g;
|
||||
const EM_TEXT_REG = /\*(.+?)\*/g;
|
||||
@@ -31,32 +28,4 @@ const parseHtmlToRawText = (htmlStr: string): string => {
|
||||
return text;
|
||||
};
|
||||
|
||||
interface FormatterConfig {
|
||||
inlineImage: boolean;
|
||||
}
|
||||
const defaultFormatterConfig: FormatterConfig = {
|
||||
inlineImage: false,
|
||||
};
|
||||
|
||||
const formatMemoContent = (content: string, addtionConfig?: Partial<FormatterConfig>) => {
|
||||
const config = {
|
||||
...defaultFormatterConfig,
|
||||
...addtionConfig,
|
||||
};
|
||||
const tempElement = document.createElement("div");
|
||||
tempElement.innerHTML = parseMarkedToHtml(escape(content));
|
||||
|
||||
let outputString = tempElement.innerHTML;
|
||||
if (config.inlineImage) {
|
||||
outputString = outputString.replace(IMAGE_URL_REG, "<img class='img' src='$1' />");
|
||||
} else {
|
||||
outputString = outputString.replace(IMAGE_URL_REG, "");
|
||||
}
|
||||
|
||||
return outputString
|
||||
.replace(MEMO_LINK_REG, "<span class='memo-link-text' data-value='$2'>$1</span>")
|
||||
.replace(LINK_URL_REG, "<a class='link' target='_blank' rel='noreferrer' href='$2'>$1</a>")
|
||||
.replace(TAG_REG, "<span class='tag-span'>#$1</span> ");
|
||||
};
|
||||
|
||||
export { formatMemoContent, parseHtmlToRawText };
|
||||
export { parseMarkedToHtml, parseHtmlToRawText };
|
||||
|
||||
@@ -7,14 +7,17 @@
|
||||
@apply w-128 max-w-full mb-8;
|
||||
|
||||
> .dialog-content-container {
|
||||
@apply w-full flex flex-col justify-start items-start;
|
||||
.flex(column, flex-start, flex-start);
|
||||
@apply w-full overflow-y-auto;
|
||||
|
||||
> .tip-text-container {
|
||||
@apply w-full h-32 flex flex-col justify-center items-center;
|
||||
@apply w-full h-32;
|
||||
.flex(column, center, center);
|
||||
}
|
||||
|
||||
> .archived-memos-container {
|
||||
@apply w-full flex flex-col justify-start items-start;
|
||||
.flex(column, flex-start, flex-start);
|
||||
@apply w-full;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
@import "./mixin.less";
|
||||
|
||||
.dialog-wrapper {
|
||||
@apply fixed top-0 left-0 flex flex-col justify-start items-center w-full h-full pt-16 z-100 overflow-x-hidden overflow-y-scroll bg-transparent transition-all;
|
||||
.hide-scroll-bar();
|
||||
|
||||
&.showup {
|
||||
background-color: rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
&.showoff {
|
||||
display: none;
|
||||
}
|
||||
|
||||
> .dialog-container {
|
||||
@apply flex flex-col justify-start items-start bg-white p-4 rounded-lg;
|
||||
|
||||
> .dialog-header-container {
|
||||
@apply flex flex-row justify-between items-center w-full mb-4;
|
||||
|
||||
> .title-text {
|
||||
> .icon-text {
|
||||
@apply mr-2 text-base;
|
||||
}
|
||||
}
|
||||
|
||||
.btn {
|
||||
@apply flex flex-col justify-center items-center w-6 h-6 rounded hover:bg-gray-100 hover:shadow;
|
||||
}
|
||||
}
|
||||
|
||||
> .dialog-content-container {
|
||||
@apply flex flex-col justify-start items-start w-full;
|
||||
}
|
||||
|
||||
> .dialog-footer-container {
|
||||
@apply flex flex-row justify-end items-center w-full mt-4;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
@import "./mixin.less";
|
||||
|
||||
.common-dialog {
|
||||
> .dialog-container {
|
||||
@apply w-80;
|
||||
|
||||
> .dialog-content-container {
|
||||
@apply flex flex-col justify-start items-start;
|
||||
|
||||
> .btns-container {
|
||||
@apply flex flex-row justify-end items-center w-full mt-4;
|
||||
|
||||
> .btn {
|
||||
@apply text-sm py-1 px-3 mr-2 rounded-md hover:opacity-80;
|
||||
|
||||
&.confirm-btn {
|
||||
@apply bg-red-100 border border-solid border-blue-600 text-blue-600;
|
||||
|
||||
&.warning {
|
||||
@apply border-red-600 text-red-600;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
@import "../mixin.less";
|
||||
|
||||
.dropdown-wrapper {
|
||||
@apply relative flex flex-col justify-start items-start select-none;
|
||||
|
||||
> .trigger-button {
|
||||
@apply flex flex-row justify-center items-center border p-1 rounded shadow text-gray-600 cursor-pointer hover:opacity-80;
|
||||
|
||||
> .icon-img {
|
||||
@apply w-4 h-auto;
|
||||
}
|
||||
}
|
||||
|
||||
> .action-buttons-container {
|
||||
@apply w-28 mt-1 absolute top-full right-0 flex flex-col justify-start items-start bg-white z-1 border p-1 rounded shadow;
|
||||
|
||||
> button {
|
||||
@apply w-full text-left px-2 text-sm leading-7 rounded hover:bg-gray-100;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
@import "./mixin.less";
|
||||
|
||||
.confirm-reset-openid-dialog {
|
||||
> .dialog-container {
|
||||
@apply w-80;
|
||||
|
||||
> .dialog-content-container {
|
||||
.flex(column, flex-start, flex-start);
|
||||
|
||||
> .warn-text {
|
||||
@apply pt-2;
|
||||
}
|
||||
|
||||
> .btns-container {
|
||||
.flex(row, flex-end, center);
|
||||
@apply w-full mt-3;
|
||||
|
||||
> .btn {
|
||||
@apply text-sm py-1 px-3 mr-2 rounded-md;
|
||||
|
||||
&:hover {
|
||||
@apply opacity-80;
|
||||
}
|
||||
|
||||
&.confirm-btn {
|
||||
@apply bg-red-100 border border-solid border-red-600 text-red-600;
|
||||
|
||||
&.loading {
|
||||
@apply opacity-80 cursor-wait;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,13 +4,13 @@
|
||||
@apply p-0 sm:py-16;
|
||||
|
||||
> .dialog-container {
|
||||
@apply w-full sm:w-112 max-w-full grow sm:grow-0 bg-white p-0 rounded-none sm:rounded-lg;
|
||||
@apply w-112 max-w-full grow sm:grow-0 bg-white p-0 rounded-none sm:rounded-lg;
|
||||
|
||||
> .dialog-header-container {
|
||||
@apply relative flex flex-row justify-between items-center w-full p-6 pb-0 mb-0;
|
||||
|
||||
> .title-text {
|
||||
@apply px-2 py-1 -ml-2 cursor-pointer select-none rounded hover:bg-gray-100;
|
||||
@apply cursor-pointer select-none rounded hover:bg-gray-100;
|
||||
}
|
||||
|
||||
> .btns-container {
|
||||
@@ -23,14 +23,10 @@
|
||||
@apply w-full h-auto;
|
||||
}
|
||||
|
||||
&.share {
|
||||
&.share-btn {
|
||||
@apply ~"p-0.5";
|
||||
}
|
||||
}
|
||||
|
||||
> .split-line {
|
||||
@apply font-mono text-gray-300 mr-2;
|
||||
}
|
||||
}
|
||||
|
||||
> .date-picker {
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
@import "./mixin.less";
|
||||
|
||||
.dialog-wrapper {
|
||||
.flex(column, flex-start, center);
|
||||
@apply fixed top-0 left-0 w-full h-full pt-16 z-1000 overflow-x-hidden overflow-y-scroll bg-transparent transition-all;
|
||||
.hide-scroll-bar();
|
||||
|
||||
&.showup {
|
||||
background-color: rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
&.showoff {
|
||||
display: none;
|
||||
}
|
||||
|
||||
> .dialog-container {
|
||||
.flex(column, flex-start, flex-start);
|
||||
@apply bg-white p-4 rounded-lg;
|
||||
|
||||
> .dialog-header-container {
|
||||
.flex(row, space-between, center);
|
||||
@apply w-full mb-4;
|
||||
|
||||
> .title-text {
|
||||
> .icon-text {
|
||||
@apply mr-2 text-base;
|
||||
}
|
||||
}
|
||||
|
||||
.btn {
|
||||
.flex(column, center, center);
|
||||
@apply w-6 h-6 rounded hover:bg-gray-100 hover:shadow;
|
||||
}
|
||||
}
|
||||
|
||||
> .dialog-content-container {
|
||||
.flex(column, flex-start, flex-start);
|
||||
@apply w-full;
|
||||
}
|
||||
|
||||
> .dialog-footer-container {
|
||||
.flex(row, flex-end, center);
|
||||
@apply w-full mt-4;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
@apply w-auto h-full px-2 border-r rounded-l flex flex-row justify-center items-center text-xs font-bold text-gray-800 bg-gray-100;
|
||||
|
||||
> .icon-img {
|
||||
@apply mr-1 w-4 h-4;
|
||||
@apply mr-1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,18 +18,6 @@
|
||||
@apply sticky top-0 w-full h-full flex flex-col justify-start items-start z-10;
|
||||
background-color: #f6f5f4;
|
||||
}
|
||||
|
||||
> .addtion-btn-container {
|
||||
@apply fixed bottom-12 left-1/2 -translate-x-1/2;
|
||||
|
||||
> .btn {
|
||||
@apply bg-blue-600 text-white px-4 py-2 rounded-3xl shadow-2xl hover:opacity-80;
|
||||
|
||||
> .icon {
|
||||
@apply text-lg mr-1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,35 +6,17 @@
|
||||
> .dialog-container {
|
||||
@apply w-full p-0 bg-transparent flex flex-col justify-start items-center;
|
||||
|
||||
> .card-header-container {
|
||||
@apply z-10 w-128 max-w-full flex flex-row justify-start items-center mb-2;
|
||||
|
||||
> .visibility-selector-container {
|
||||
@apply bg-white px-2 pl-3 py-1 rounded-lg flex flex-row justify-start items-center;
|
||||
|
||||
> .icon-img {
|
||||
@apply mr-1 w-4 h-auto;
|
||||
}
|
||||
|
||||
> .visibility-selector {
|
||||
@apply w-32;
|
||||
|
||||
> .current-value-container {
|
||||
@apply border-none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .memo-card-container {
|
||||
@apply flex flex-col justify-start items-start relative w-128 max-w-full py-3 px-6 mb-3 rounded-lg bg-yellow-200;
|
||||
.flex(column, flex-start, flex-start);
|
||||
@apply relative w-128 max-w-full py-3 px-6 mb-3 rounded-lg bg-yellow-200;
|
||||
|
||||
> * {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
> .header-container {
|
||||
@apply flex flex-row justify-between items-center w-full h-auto pb-0 my-0;
|
||||
.flex(row, space-between, center);
|
||||
@apply w-full h-auto pb-0 my-0;
|
||||
|
||||
> .time-text {
|
||||
@apply text-sm text-gray-500 font-mono;
|
||||
@@ -45,11 +27,7 @@
|
||||
|
||||
> .btn {
|
||||
.flex(row, center, center);
|
||||
@apply w-6 h-6 p-1 ml-2 rounded text-gray-600 hover:bg-white;
|
||||
}
|
||||
|
||||
> .split-line {
|
||||
@apply font-mono text-gray-300 ml-2;
|
||||
@apply w-6 h-6 ml-2 rounded text-gray-600 hover:bg-white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,10 +7,6 @@
|
||||
@apply inline-block w-full h-auto mb-1 last:mb-0 text-base leading-7 whitespace-pre-wrap break-all;
|
||||
}
|
||||
|
||||
.img {
|
||||
@apply float-left max-w-full w-full;
|
||||
}
|
||||
|
||||
.tag-span {
|
||||
@apply inline-block w-auto font-mono text-blue-600;
|
||||
}
|
||||
|
||||
+3
-11
@@ -24,15 +24,7 @@
|
||||
}
|
||||
|
||||
> .status-text {
|
||||
@apply text-xs cursor-pointer ml-2 rounded border px-1;
|
||||
|
||||
&.public {
|
||||
@apply border-green-600 text-green-600;
|
||||
}
|
||||
|
||||
&.protected {
|
||||
@apply border-gray-400 text-gray-400;
|
||||
}
|
||||
@apply text-xs cursor-pointer ml-2 rounded border border-green-600 px-1 text-green-600;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,10 +43,10 @@
|
||||
box-shadow: 0 0 8px 0 rgb(0 0 0 / 20%);
|
||||
|
||||
> .btns-container {
|
||||
@apply w-full flex flex-row justify-around items-center border-b border-gray-100 p-1 mb-1;
|
||||
@apply w-full flex flex-row justify-between items-center border-b border-gray-100 p-1 mb-1;
|
||||
|
||||
> .btn {
|
||||
@apply relative w-6 h-6 p-1 text-gray-600;
|
||||
@apply relative w-7 h-7 p-1 text-gray-600;
|
||||
|
||||
&:hover > .tip-text {
|
||||
@apply block;
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
|
||||
.preview-image-dialog {
|
||||
@apply p-0;
|
||||
z-index: 101;
|
||||
background-color: rgba(0, 0, 0, 0.6);
|
||||
|
||||
> .dialog-container {
|
||||
@apply flex flex-col justify-center items-center relative w-full h-full p-0;
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
@import "./mixin.less";
|
||||
|
||||
.resources-dialog {
|
||||
@apply px-4;
|
||||
|
||||
> .dialog-container {
|
||||
@apply w-128 max-w-full mb-8;
|
||||
|
||||
> .dialog-content-container {
|
||||
@apply flex flex-col justify-start items-start w-full;
|
||||
|
||||
> .tip-text-container {
|
||||
@apply w-full flex flex-row justify-start items-start border border-yellow-600 rounded px-2 py-1 mb-2 text-yellow-600 bg-yellow-50 text-sm;
|
||||
}
|
||||
|
||||
> .upload-resource-container {
|
||||
@apply mt-2 mb-4 w-full rounded flex flex-row justify-start items-center;
|
||||
|
||||
> .upload-resource-btn {
|
||||
@apply px-3 py-1 rounded cursor-pointer flex flex-row justify-center items-center border border-dashed border-blue-600 text-blue-600 bg-blue-50 hover:opacity-80;
|
||||
|
||||
> .icon-img {
|
||||
@apply w-4 h-auto mr-1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .loading-text-container {
|
||||
@apply flex flex-col justify-center items-center w-full h-32;
|
||||
}
|
||||
|
||||
> .resource-table-container {
|
||||
@apply flex flex-col justify-start items-start w-full;
|
||||
|
||||
> .fields-container {
|
||||
@apply px-2 py-2 w-full grid grid-cols-5 border-b;
|
||||
|
||||
> .field-text {
|
||||
@apply font-mono text-gray-400;
|
||||
}
|
||||
}
|
||||
|
||||
> .tip-text {
|
||||
@apply w-full text-center text-base my-6 mt-8;
|
||||
}
|
||||
|
||||
> .resource-container {
|
||||
@apply px-2 py-2 w-full grid grid-cols-5;
|
||||
|
||||
> .buttons-container {
|
||||
@apply w-full flex flex-row justify-end items-center;
|
||||
|
||||
> .actions-dropdown {
|
||||
.delete-btn {
|
||||
@apply text-red-600;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.field-text {
|
||||
@apply w-full truncate text-base pr-2 last:pr-0;
|
||||
|
||||
&.name-text {
|
||||
@apply col-span-2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,28 +1,24 @@
|
||||
@import "./mixin.less";
|
||||
|
||||
.search-bar-container {
|
||||
@apply relative w-auto;
|
||||
|
||||
&:hover,
|
||||
&:active {
|
||||
> .search-bar-inputer > .text-input {
|
||||
@apply flex;
|
||||
}
|
||||
|
||||
> .quickly-action-wrapper {
|
||||
@apply flex;
|
||||
}
|
||||
}
|
||||
@apply relative w-40;
|
||||
|
||||
> .search-bar-inputer {
|
||||
@apply h-9 flex flex-row justify-start items-center w-full py-2 px-3 sm:px-4 rounded-full sm:rounded-lg bg-zinc-200;
|
||||
.flex(row, flex-start, center);
|
||||
@apply w-full py-2 px-4 rounded-lg flex flex-row justify-start items-center bg-zinc-200;
|
||||
|
||||
> .icon-img {
|
||||
@apply w-4 h-auto opacity-30;
|
||||
@apply mr-2 h-auto opacity-30;
|
||||
}
|
||||
|
||||
> .text-input {
|
||||
@apply hidden sm:flex ml-2 w-24 grow text-sm;
|
||||
@apply grow text-sm;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
+ .quickly-action-wrapper {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,40 +26,66 @@
|
||||
@apply hidden absolute top-9 -right-2 p-2 w-80 z-10;
|
||||
|
||||
> .quickly-action-container {
|
||||
@apply flex flex-col justify-start items-start w-full bg-white px-4 py-3 rounded-lg;
|
||||
.flex(column, flex-start, flex-start);
|
||||
width: 100%;
|
||||
background-color: white;
|
||||
padding: 12px 16px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 0 8px 0 rgb(0 0 0 / 20%);
|
||||
|
||||
> .title-text {
|
||||
@apply text-gray-600 text-xs;
|
||||
color: gray;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
> .types-container {
|
||||
@apply flex flex-row justify-start items-start w-full text-xs mt-2;
|
||||
.flex(row, flex-start, flex-start);
|
||||
width: 100%;
|
||||
font-size: 13px;
|
||||
margin-top: 8px;
|
||||
|
||||
> .section-text {
|
||||
@apply text-gray-600 mr-1 shrink-0 leading-6;
|
||||
color: gray;
|
||||
margin-right: 4px;
|
||||
flex-shrink: 0;
|
||||
line-height: 26px;
|
||||
}
|
||||
|
||||
> .values-container {
|
||||
@apply flex flex-row justify-start items-start flex-wrap select-none;
|
||||
.flex(row, flex-start, flex-start);
|
||||
flex-wrap: wrap;
|
||||
user-select: none;
|
||||
|
||||
> div {
|
||||
@apply flex flex-row justify-start items-center leading-6;
|
||||
.flex(row, flex-start, center);
|
||||
line-height: 26px;
|
||||
|
||||
.type-item {
|
||||
@apply cursor-pointer px-1 rounded hover:bg-gray-100;
|
||||
cursor: pointer;
|
||||
padding: 0 4px;
|
||||
border-radius: 6px;
|
||||
|
||||
&:hover {
|
||||
background-color: @bg-whitegray;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
@apply bg-green-600 text-white;
|
||||
background-color: @text-green;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
.split-text {
|
||||
@apply text-gray-400 mx-1;
|
||||
color: lightgray;
|
||||
margin: 0 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,32 +32,22 @@
|
||||
}
|
||||
|
||||
> .member-container {
|
||||
@apply w-full grid grid-cols-4 border-b py-2;
|
||||
@apply w-full grid grid-cols-5 border-b py-2;
|
||||
|
||||
> .field-text {
|
||||
@apply text-base pl-2 mr-4 w-16 truncate;
|
||||
@apply text-base pl-2 mr-4 w-16;
|
||||
|
||||
&.id-text {
|
||||
@apply font-mono text-gray-600;
|
||||
}
|
||||
|
||||
&.email-text {
|
||||
@apply w-auto col-span-2;
|
||||
@apply col-span-3;
|
||||
}
|
||||
}
|
||||
|
||||
> .buttons-container {
|
||||
@apply col-span-1 flex flex-row justify-end items-center;
|
||||
|
||||
> .tip-text {
|
||||
@apply text-gray-400;
|
||||
}
|
||||
|
||||
> .actions-dropdown {
|
||||
.delete {
|
||||
@apply text-red-600;
|
||||
}
|
||||
}
|
||||
@apply col-span-1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,10 +12,10 @@
|
||||
}
|
||||
|
||||
> .btn {
|
||||
@apply flex flex-col justify-center items-center w-5 h-5 bg-gray-200 rounded ml-2 shadow hover:opacity-80;
|
||||
@apply flex flex-col justify-center items-center w-5 p-1 h-5 bg-gray-200 rounded ml-2 shadow hover:opacity-80;
|
||||
|
||||
> .icon-img {
|
||||
@apply w-4 h-4;
|
||||
@apply opacity-60;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,7 @@
|
||||
@apply flex flex-col justify-start items-start relative w-full h-auto flex-nowrap mb-2;
|
||||
|
||||
> .shortcut-container {
|
||||
@apply relative flex flex-row justify-between items-center w-full h-10 py-0 px-4 mt-px first:mt-2 rounded-lg text-base cursor-pointer select-none shrink-0 hover:bg-white;
|
||||
@apply flex flex-row justify-between items-center w-full h-10 py-0 px-4 mt-px first:mt-2 rounded-lg text-base cursor-pointer select-none shrink-0 hover:bg-white;
|
||||
|
||||
&:hover {
|
||||
> .btns-container {
|
||||
@@ -57,7 +57,7 @@
|
||||
@apply flex flex-row justify-center items-center w-6 h-6 shrink-0;
|
||||
|
||||
&.toggle-btn {
|
||||
@apply w-4 h-auto text-gray-600;
|
||||
@apply opacity-60;
|
||||
|
||||
&:hover {
|
||||
& + .action-btns-wrapper {
|
||||
@@ -68,7 +68,7 @@
|
||||
}
|
||||
|
||||
> .action-btns-wrapper {
|
||||
@apply absolute top-6 right-0 flex-col justify-start items-start w-auto h-auto px-4 pt-3 hidden z-1;
|
||||
@apply flex-col justify-start items-start absolute top-6 right-0 w-auto h-auto px-4 pt-3 hidden z-1;
|
||||
|
||||
> .action-btns-container {
|
||||
@apply flex flex-col justify-start items-start w-24 h-auto p-1 whitespace-nowrap rounded-md bg-white;
|
||||
|
||||
+9
-1
@@ -1,9 +1,10 @@
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { Provider } from "react-redux";
|
||||
import store from "./store";
|
||||
import { updateStateWithLocation } from "./store/modules/location";
|
||||
import App from "./App";
|
||||
import "./helpers/polyfill";
|
||||
import "./less/global.less";
|
||||
import "./helpers/polyfill";
|
||||
import "./css/index.css";
|
||||
|
||||
const container = document.getElementById("root");
|
||||
@@ -13,3 +14,10 @@ root.render(
|
||||
<App />
|
||||
</Provider>
|
||||
);
|
||||
|
||||
window.onload = () => {
|
||||
store.dispatch(updateStateWithLocation());
|
||||
window.onpopstate = () => {
|
||||
store.dispatch(updateStateWithLocation());
|
||||
};
|
||||
};
|
||||
|
||||
@@ -12,7 +12,6 @@ import toastHelper from "../components/Toast";
|
||||
import "../less/home.less";
|
||||
|
||||
function Home() {
|
||||
const user = useAppSelector((state) => state.user.user);
|
||||
const location = useAppSelector((state) => state.location);
|
||||
const loadingState = useLoading();
|
||||
|
||||
@@ -54,19 +53,6 @@ function Home() {
|
||||
<MemoFilter />
|
||||
</div>
|
||||
<MemoList />
|
||||
<Only when={userService.isVisitorMode()}>
|
||||
<div className="addtion-btn-container">
|
||||
{user ? (
|
||||
<button className="btn" onClick={() => (window.location.href = "/")}>
|
||||
<span className="icon">🏠</span> Back to Home
|
||||
</button>
|
||||
) : (
|
||||
<button className="btn" onClick={() => (window.location.href = "/signin")}>
|
||||
<span className="icon">👉</span> Sign in
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</Only>
|
||||
</main>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { stringify } from "qs";
|
||||
import store from "../store";
|
||||
import { setQuery, setPathname, Query, updateStateWithLocation } from "../store/modules/location";
|
||||
import { setQuery, setPathname, Query } from "../store/modules/location";
|
||||
|
||||
const updateLocationUrl = (method: "replace" | "push" = "replace") => {
|
||||
const { query, pathname, hash } = store.getState().location;
|
||||
@@ -23,10 +23,6 @@ const locationService = {
|
||||
return store.getState().location;
|
||||
},
|
||||
|
||||
updateStateWithLocation: () => {
|
||||
store.dispatch(updateStateWithLocation());
|
||||
},
|
||||
|
||||
setPathname: (pathname: string) => {
|
||||
store.dispatch(setPathname(pathname));
|
||||
updateLocationUrl();
|
||||
|
||||
@@ -1,19 +1,6 @@
|
||||
import * as api from "../helpers/api";
|
||||
|
||||
const convertResponseModelResource = (resource: Resource): Resource => {
|
||||
return {
|
||||
...resource,
|
||||
createdTs: resource.createdTs * 1000,
|
||||
updatedTs: resource.updatedTs * 1000,
|
||||
};
|
||||
};
|
||||
|
||||
const resourceService = {
|
||||
async getResourceList(): Promise<Resource[]> {
|
||||
const { data } = (await api.getResourceList()).data;
|
||||
const resourceList = data.map((m) => convertResponseModelResource(m));
|
||||
return resourceList;
|
||||
},
|
||||
/**
|
||||
* Upload resource file to server,
|
||||
* @param file file
|
||||
@@ -32,9 +19,6 @@ const resourceService = {
|
||||
|
||||
return data;
|
||||
},
|
||||
async deleteResourceById(id: ResourceId) {
|
||||
return api.deleteResourceById(id);
|
||||
},
|
||||
};
|
||||
|
||||
export default resourceService;
|
||||
|
||||
@@ -33,7 +33,7 @@ const userService = {
|
||||
}
|
||||
}
|
||||
|
||||
const { data: user } = (await api.getMyselfUser()).data;
|
||||
const { data: user } = (await api.getUser()).data;
|
||||
if (user) {
|
||||
store.dispatch(setUser(convertResponseModelUser(user)));
|
||||
}
|
||||
@@ -53,7 +53,7 @@ const userService = {
|
||||
},
|
||||
|
||||
doSignIn: async () => {
|
||||
const { data: user } = (await api.getMyselfUser()).data;
|
||||
const { data: user } = (await api.getUser()).data;
|
||||
if (user) {
|
||||
store.dispatch(setUser(convertResponseModelUser(user)));
|
||||
} else {
|
||||
@@ -78,14 +78,8 @@ const userService = {
|
||||
|
||||
patchUser: async (userPatch: UserPatch): Promise<void> => {
|
||||
const { data } = (await api.patchUser(userPatch)).data;
|
||||
if (userPatch.id === store.getState().user.user?.id) {
|
||||
const user = convertResponseModelUser(data);
|
||||
store.dispatch(patchUser(user));
|
||||
}
|
||||
},
|
||||
|
||||
deleteUser: async (userDelete: UserDelete) => {
|
||||
await api.deleteUser(userDelete);
|
||||
const user = convertResponseModelUser(data);
|
||||
store.dispatch(patchUser(user));
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -64,20 +64,12 @@ const locationSlice = createSlice({
|
||||
return getStateFromLocation();
|
||||
},
|
||||
setPathname: (state, action: PayloadAction<string>) => {
|
||||
if (state.pathname === action.payload) {
|
||||
return state;
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
pathname: action.payload,
|
||||
};
|
||||
},
|
||||
setQuery: (state, action: PayloadAction<Partial<Query>>) => {
|
||||
if (JSON.stringify(action.payload) === state.query) {
|
||||
return state;
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
query: {
|
||||
|
||||
Vendored
+1
-1
@@ -1,6 +1,6 @@
|
||||
type MemoId = number;
|
||||
|
||||
type Visibility = "PUBLIC" | "PROTECTED" | "PRIVATE";
|
||||
type Visibility = "PUBLIC" | "PRIVATE";
|
||||
|
||||
interface Memo {
|
||||
id: MemoId;
|
||||
|
||||
Vendored
+1
-1
@@ -1,7 +1,7 @@
|
||||
type ResourceId = number;
|
||||
|
||||
interface Resource {
|
||||
id: ResourceId;
|
||||
id: string;
|
||||
|
||||
createdTs: TimeStamp;
|
||||
updatedTs: TimeStamp;
|
||||
|
||||
Vendored
-8
@@ -22,15 +22,7 @@ interface UserCreate {
|
||||
}
|
||||
|
||||
interface UserPatch {
|
||||
id: UserId;
|
||||
|
||||
rowStatus?: RowStatus;
|
||||
|
||||
name?: string;
|
||||
password?: string;
|
||||
resetOpenId?: boolean;
|
||||
}
|
||||
|
||||
interface UserDelete {
|
||||
id: UserId;
|
||||
}
|
||||
|
||||
+1
-8
@@ -2028,7 +2028,7 @@ prettier@2.5.1:
|
||||
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.5.1.tgz#fff75fa9d519c54cf0fce328c1017d94546bc56a"
|
||||
integrity sha512-vBZcPRUR5MZJwoyi3ZoyQlc1rXeEck8KgeC9AwwOn+exuxLxq5toTRDTSaVrXHxelDMHy9zlicw8u66yxoSUFg==
|
||||
|
||||
prop-types@^15.7.2, prop-types@^15.8.1:
|
||||
prop-types@^15.8.1:
|
||||
version "15.8.1"
|
||||
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
|
||||
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
|
||||
@@ -2072,13 +2072,6 @@ react-dom@^18.1.0:
|
||||
loose-envify "^1.1.0"
|
||||
scheduler "^0.23.0"
|
||||
|
||||
react-feather@^2.0.10:
|
||||
version "2.0.10"
|
||||
resolved "https://registry.yarnpkg.com/react-feather/-/react-feather-2.0.10.tgz#0e9abf05a66754f7b7bb71757ac4da7fb6be3b68"
|
||||
integrity sha512-BLhukwJ+Z92Nmdcs+EMw6dy1Z/VLiJTzEQACDUEnWMClhYnFykJCGWQx+NmwP/qQHGX/5CzQ+TGi8ofg2+HzVQ==
|
||||
dependencies:
|
||||
prop-types "^15.7.2"
|
||||
|
||||
react-is@^16.13.1, react-is@^16.7.0:
|
||||
version "16.13.1"
|
||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
|
||||
|
||||
Reference in New Issue
Block a user