mirror of
https://github.com/usememos/memos.git
synced 2024-11-28 05:53:14 +03:00
feat: add public id field to resource (#1451)
* feat: add public id field to resource * feat: support reset resource link
This commit is contained in:
parent
c9a5df81ce
commit
1cab30f32f
@ -15,6 +15,7 @@ type Resource struct {
|
|||||||
ExternalLink string `json:"externalLink"`
|
ExternalLink string `json:"externalLink"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Size int64 `json:"size"`
|
Size int64 `json:"size"`
|
||||||
|
PublicID string `json:"publicId"`
|
||||||
|
|
||||||
// Related fields
|
// Related fields
|
||||||
LinkedMemoAmount int `json:"linkedMemoAmount"`
|
LinkedMemoAmount int `json:"linkedMemoAmount"`
|
||||||
@ -31,6 +32,7 @@ type ResourceCreate struct {
|
|||||||
ExternalLink string `json:"externalLink"`
|
ExternalLink string `json:"externalLink"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Size int64 `json:"-"`
|
Size int64 `json:"-"`
|
||||||
|
PublicID string `json:"publicId"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ResourceFind struct {
|
type ResourceFind struct {
|
||||||
@ -42,6 +44,7 @@ type ResourceFind struct {
|
|||||||
// Domain specific fields
|
// Domain specific fields
|
||||||
Filename *string `json:"filename"`
|
Filename *string `json:"filename"`
|
||||||
MemoID *int
|
MemoID *int
|
||||||
|
PublicID *string `json:"publicId"`
|
||||||
GetBlob bool
|
GetBlob bool
|
||||||
|
|
||||||
// Pagination
|
// Pagination
|
||||||
@ -57,6 +60,8 @@ type ResourcePatch struct {
|
|||||||
|
|
||||||
// Domain specific fields
|
// Domain specific fields
|
||||||
Filename *string `json:"filename"`
|
Filename *string `json:"filename"`
|
||||||
|
ResetPublicID *bool `json:"resetPublicId"`
|
||||||
|
PublicID *string `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ResourceDelete struct {
|
type ResourceDelete struct {
|
||||||
|
@ -122,6 +122,10 @@ func JWTMiddleware(server *Server, next echo.HandlerFunc, secret string) echo.Ha
|
|||||||
|
|
||||||
token := findAccessToken(c)
|
token := findAccessToken(c)
|
||||||
if token == "" {
|
if token == "" {
|
||||||
|
// Allow the user to access the public endpoints.
|
||||||
|
if common.HasPrefixes(path, "/o") {
|
||||||
|
return next(c)
|
||||||
|
}
|
||||||
// When the request is not authenticated, we allow the user to access the memo endpoints for those public memos.
|
// When the request is not authenticated, we allow the user to access the memo endpoints for those public memos.
|
||||||
if common.HasPrefixes(path, "/api/memo") && method == http.MethodGet {
|
if common.HasPrefixes(path, "/api/memo") && method == http.MethodGet {
|
||||||
return next(c)
|
return next(c)
|
||||||
|
@ -194,6 +194,11 @@ func (s *Server) registerResourceRoutes(g *echo.Group) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if s.Profile.IsDev() {
|
||||||
|
publicID := common.GenUUID()
|
||||||
|
resourceCreate.PublicID = publicID
|
||||||
|
}
|
||||||
|
|
||||||
resource, err := s.Store.CreateResource(ctx, resourceCreate)
|
resource, err := s.Store.CreateResource(ctx, resourceCreate)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create resource").SetInternal(err)
|
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create resource").SetInternal(err)
|
||||||
@ -227,52 +232,6 @@ func (s *Server) registerResourceRoutes(g *echo.Group) {
|
|||||||
return c.JSON(http.StatusOK, composeResponse(list))
|
return c.JSON(http.StatusOK, composeResponse(list))
|
||||||
})
|
})
|
||||||
|
|
||||||
g.GET("/resource/:resourceId", func(c echo.Context) error {
|
|
||||||
ctx := c.Request().Context()
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
userID, ok := c.Get(getUserIDContextKey()).(int)
|
|
||||||
if !ok {
|
|
||||||
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
|
|
||||||
}
|
|
||||||
resourceFind := &api.ResourceFind{
|
|
||||||
ID: &resourceID,
|
|
||||||
CreatorID: &userID,
|
|
||||||
GetBlob: true,
|
|
||||||
}
|
|
||||||
resource, err := s.Store.FindResource(ctx, resourceFind)
|
|
||||||
if err != nil {
|
|
||||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch resource").SetInternal(err)
|
|
||||||
}
|
|
||||||
return c.JSON(http.StatusOK, composeResponse(resource))
|
|
||||||
})
|
|
||||||
|
|
||||||
g.GET("/resource/:resourceId/blob", func(c echo.Context) error {
|
|
||||||
ctx := c.Request().Context()
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
userID, ok := c.Get(getUserIDContextKey()).(int)
|
|
||||||
if !ok {
|
|
||||||
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
|
|
||||||
}
|
|
||||||
resourceFind := &api.ResourceFind{
|
|
||||||
ID: &resourceID,
|
|
||||||
CreatorID: &userID,
|
|
||||||
GetBlob: true,
|
|
||||||
}
|
|
||||||
resource, err := s.Store.FindResource(ctx, resourceFind)
|
|
||||||
if err != nil {
|
|
||||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch resource").SetInternal(err)
|
|
||||||
}
|
|
||||||
return c.Stream(http.StatusOK, resource.Type, bytes.NewReader(resource.Blob))
|
|
||||||
})
|
|
||||||
|
|
||||||
g.PATCH("/resource/:resourceId", func(c echo.Context) error {
|
g.PATCH("/resource/:resourceId", func(c echo.Context) error {
|
||||||
ctx := c.Request().Context()
|
ctx := c.Request().Context()
|
||||||
userID, ok := c.Get(getUserIDContextKey()).(int)
|
userID, ok := c.Get(getUserIDContextKey()).(int)
|
||||||
@ -304,6 +263,11 @@ func (s *Server) registerResourceRoutes(g *echo.Group) {
|
|||||||
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted patch resource request").SetInternal(err)
|
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted patch resource request").SetInternal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if resourcePatch.ResetPublicID != nil && *resourcePatch.ResetPublicID {
|
||||||
|
publicID := common.GenUUID()
|
||||||
|
resourcePatch.PublicID = &publicID
|
||||||
|
}
|
||||||
|
|
||||||
resourcePatch.ID = resourceID
|
resourcePatch.ID = resourceID
|
||||||
resource, err = s.Store.PatchResource(ctx, resourcePatch)
|
resource, err = s.Store.PatchResource(ctx, resourcePatch)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -349,19 +313,19 @@ func (s *Server) registerResourceRoutes(g *echo.Group) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) registerResourcePublicRoutes(g *echo.Group) {
|
func (s *Server) registerResourcePublicRoutes(g *echo.Group) {
|
||||||
g.GET("/r/:resourceId/:filename", func(c echo.Context) error {
|
g.GET("/r/:resourceId/:publicId", func(c echo.Context) error {
|
||||||
ctx := c.Request().Context()
|
ctx := c.Request().Context()
|
||||||
resourceID, err := strconv.Atoi(c.Param("resourceId"))
|
resourceID, err := strconv.Atoi(c.Param("resourceId"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("resourceId"))).SetInternal(err)
|
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("resourceId"))).SetInternal(err)
|
||||||
}
|
}
|
||||||
filename, err := url.QueryUnescape(c.Param("filename"))
|
publicID, err := url.QueryUnescape(c.Param("publicId"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("filename is invalid: %s", c.Param("filename"))).SetInternal(err)
|
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("publicID is invalid: %s", c.Param("publicId"))).SetInternal(err)
|
||||||
}
|
}
|
||||||
resourceFind := &api.ResourceFind{
|
resourceFind := &api.ResourceFind{
|
||||||
ID: &resourceID,
|
ID: &resourceID,
|
||||||
Filename: &filename,
|
PublicID: &publicID,
|
||||||
GetBlob: true,
|
GetBlob: true,
|
||||||
}
|
}
|
||||||
resource, err := s.Store.FindResource(ctx, resourceFind)
|
resource, err := s.Store.FindResource(ctx, resourceFind)
|
||||||
@ -369,6 +333,10 @@ func (s *Server) registerResourcePublicRoutes(g *echo.Group) {
|
|||||||
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to find resource by ID: %v", resourceID)).SetInternal(err)
|
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to find resource by ID: %v", resourceID)).SetInternal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if resource.ExternalLink != "" {
|
||||||
|
return c.Redirect(http.StatusSeeOther, resource.ExternalLink)
|
||||||
|
}
|
||||||
|
|
||||||
blob := resource.Blob
|
blob := resource.Blob
|
||||||
if resource.InternalPath != "" {
|
if resource.InternalPath != "" {
|
||||||
src, err := os.Open(resource.InternalPath)
|
src, err := os.Open(resource.InternalPath)
|
||||||
|
@ -79,29 +79,32 @@ func NewServer(ctx context.Context, profile *profile.Profile) (*Server, error) {
|
|||||||
}
|
}
|
||||||
s.ID = serverID
|
s.ID = serverID
|
||||||
|
|
||||||
secretSessionName := "usememos"
|
embedFrontend(e)
|
||||||
|
|
||||||
|
secret := "usememos"
|
||||||
if profile.Mode == "prod" {
|
if profile.Mode == "prod" {
|
||||||
secretSessionName, err = s.getSystemSecretSessionName(ctx)
|
secret, err = s.getSystemSecretSessionName(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
embedFrontend(e)
|
|
||||||
|
|
||||||
rootGroup := e.Group("")
|
rootGroup := e.Group("")
|
||||||
s.registerRSSRoutes(rootGroup)
|
s.registerRSSRoutes(rootGroup)
|
||||||
|
|
||||||
publicGroup := e.Group("/o")
|
publicGroup := e.Group("/o")
|
||||||
s.registerResourcePublicRoutes(publicGroup)
|
publicGroup.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||||
|
return JWTMiddleware(s, next, secret)
|
||||||
|
})
|
||||||
registerGetterPublicRoutes(publicGroup)
|
registerGetterPublicRoutes(publicGroup)
|
||||||
|
s.registerResourcePublicRoutes(publicGroup)
|
||||||
|
|
||||||
apiGroup := e.Group("/api")
|
apiGroup := e.Group("/api")
|
||||||
apiGroup.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
|
apiGroup.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||||
return JWTMiddleware(s, next, secretSessionName)
|
return JWTMiddleware(s, next, secret)
|
||||||
})
|
})
|
||||||
s.registerSystemRoutes(apiGroup)
|
s.registerSystemRoutes(apiGroup)
|
||||||
s.registerAuthRoutes(apiGroup, secretSessionName)
|
s.registerAuthRoutes(apiGroup, secret)
|
||||||
s.registerUserRoutes(apiGroup)
|
s.registerUserRoutes(apiGroup)
|
||||||
s.registerMemoRoutes(apiGroup)
|
s.registerMemoRoutes(apiGroup)
|
||||||
s.registerShortcutRoutes(apiGroup)
|
s.registerShortcutRoutes(apiGroup)
|
||||||
|
@ -74,10 +74,12 @@ CREATE TABLE resource (
|
|||||||
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
|
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
|
||||||
filename TEXT NOT NULL DEFAULT '',
|
filename TEXT NOT NULL DEFAULT '',
|
||||||
blob BLOB DEFAULT NULL,
|
blob BLOB DEFAULT NULL,
|
||||||
internal_path TEXT NOT NULL DEFAULT '',
|
|
||||||
external_link TEXT NOT NULL DEFAULT '',
|
external_link TEXT NOT NULL DEFAULT '',
|
||||||
type TEXT NOT NULL DEFAULT '',
|
type TEXT NOT NULL DEFAULT '',
|
||||||
size INTEGER NOT NULL DEFAULT 0
|
size INTEGER NOT NULL DEFAULT 0,
|
||||||
|
internal_path TEXT NOT NULL DEFAULT '',
|
||||||
|
public_id TEXT NOT NULL DEFAULT '',
|
||||||
|
UNIQUE(id, public_id)
|
||||||
);
|
);
|
||||||
|
|
||||||
-- memo_resource
|
-- memo_resource
|
||||||
|
@ -0,0 +1,4 @@
|
|||||||
|
ALTER TABLE
|
||||||
|
resource
|
||||||
|
ADD
|
||||||
|
COLUMN internal_path TEXT NOT NULL DEFAULT '';
|
21
store/db/migration/prod/0.12/04__resource_public_id.sql
Normal file
21
store/db/migration/prod/0.12/04__resource_public_id.sql
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
ALTER TABLE
|
||||||
|
resource
|
||||||
|
ADD
|
||||||
|
COLUMN public_id TEXT NOT NULL DEFAULT '';
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX resource_id_public_id_unique_index ON resource (id, public_id);
|
||||||
|
|
||||||
|
UPDATE
|
||||||
|
resource
|
||||||
|
SET
|
||||||
|
public_id = (
|
||||||
|
SELECT
|
||||||
|
printf(
|
||||||
|
'%s-%s-%s-%s-%s',
|
||||||
|
lower(hex(randomblob(4))),
|
||||||
|
lower(hex(randomblob(2))),
|
||||||
|
lower(hex(randomblob(2))),
|
||||||
|
lower(hex(randomblob(2))),
|
||||||
|
lower(hex(randomblob(6)))
|
||||||
|
) as uuid
|
||||||
|
);
|
@ -28,6 +28,7 @@ type resourceRaw struct {
|
|||||||
ExternalLink string
|
ExternalLink string
|
||||||
Type string
|
Type string
|
||||||
Size int64
|
Size int64
|
||||||
|
PublicID string
|
||||||
LinkedMemoAmount int
|
LinkedMemoAmount int
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -47,6 +48,7 @@ func (raw *resourceRaw) toResource() *api.Resource {
|
|||||||
ExternalLink: raw.ExternalLink,
|
ExternalLink: raw.ExternalLink,
|
||||||
Type: raw.Type,
|
Type: raw.Type,
|
||||||
Size: raw.Size,
|
Size: raw.Size,
|
||||||
|
PublicID: raw.PublicID,
|
||||||
LinkedMemoAmount: raw.LinkedMemoAmount,
|
LinkedMemoAmount: raw.LinkedMemoAmount,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -195,9 +197,9 @@ func (s *Store) createResourceImpl(ctx context.Context, tx *sql.Tx, create *api.
|
|||||||
values := []any{create.Filename, create.Blob, create.ExternalLink, create.Type, create.Size, create.CreatorID}
|
values := []any{create.Filename, create.Blob, create.ExternalLink, create.Type, create.Size, create.CreatorID}
|
||||||
placeholders := []string{"?", "?", "?", "?", "?", "?"}
|
placeholders := []string{"?", "?", "?", "?", "?", "?"}
|
||||||
if s.profile.IsDev() {
|
if s.profile.IsDev() {
|
||||||
fields = append(fields, "internal_path")
|
fields = append(fields, "internal_path", "public_id")
|
||||||
values = append(values, create.InternalPath)
|
values = append(values, create.InternalPath, create.PublicID)
|
||||||
placeholders = append(placeholders, "?")
|
placeholders = append(placeholders, "?", "?")
|
||||||
}
|
}
|
||||||
|
|
||||||
query := `
|
query := `
|
||||||
@ -218,7 +220,7 @@ func (s *Store) createResourceImpl(ctx context.Context, tx *sql.Tx, create *api.
|
|||||||
&resourceRaw.CreatorID,
|
&resourceRaw.CreatorID,
|
||||||
}
|
}
|
||||||
if s.profile.IsDev() {
|
if s.profile.IsDev() {
|
||||||
dests = append(dests, &resourceRaw.InternalPath)
|
dests = append(dests, &resourceRaw.InternalPath, &resourceRaw.PublicID)
|
||||||
}
|
}
|
||||||
dests = append(dests, []any{&resourceRaw.CreatedTs, &resourceRaw.UpdatedTs}...)
|
dests = append(dests, []any{&resourceRaw.CreatedTs, &resourceRaw.UpdatedTs}...)
|
||||||
if err := tx.QueryRowContext(ctx, query, values...).Scan(dests...); err != nil {
|
if err := tx.QueryRowContext(ctx, query, values...).Scan(dests...); err != nil {
|
||||||
@ -237,12 +239,15 @@ func (s *Store) patchResourceImpl(ctx context.Context, tx *sql.Tx, patch *api.Re
|
|||||||
if v := patch.Filename; v != nil {
|
if v := patch.Filename; v != nil {
|
||||||
set, args = append(set, "filename = ?"), append(args, *v)
|
set, args = append(set, "filename = ?"), append(args, *v)
|
||||||
}
|
}
|
||||||
|
if v := patch.PublicID; v != nil {
|
||||||
|
set, args = append(set, "public_id = ?"), append(args, *v)
|
||||||
|
}
|
||||||
|
|
||||||
args = append(args, patch.ID)
|
args = append(args, patch.ID)
|
||||||
|
|
||||||
fields := []string{"id", "filename", "external_link", "type", "size", "creator_id", "created_ts", "updated_ts"}
|
fields := []string{"id", "filename", "external_link", "type", "size", "creator_id", "created_ts", "updated_ts"}
|
||||||
if s.profile.IsDev() {
|
if s.profile.IsDev() {
|
||||||
fields = append(fields, "internal_path")
|
fields = append(fields, "internal_path", "public_id")
|
||||||
}
|
}
|
||||||
|
|
||||||
query := `
|
query := `
|
||||||
@ -262,7 +267,7 @@ func (s *Store) patchResourceImpl(ctx context.Context, tx *sql.Tx, patch *api.Re
|
|||||||
&resourceRaw.UpdatedTs,
|
&resourceRaw.UpdatedTs,
|
||||||
}
|
}
|
||||||
if s.profile.IsDev() {
|
if s.profile.IsDev() {
|
||||||
dests = append(dests, &resourceRaw.InternalPath)
|
dests = append(dests, &resourceRaw.InternalPath, &resourceRaw.PublicID)
|
||||||
}
|
}
|
||||||
if err := tx.QueryRowContext(ctx, query, args...).Scan(dests...); err != nil {
|
if err := tx.QueryRowContext(ctx, query, args...).Scan(dests...); err != nil {
|
||||||
return nil, FormatError(err)
|
return nil, FormatError(err)
|
||||||
@ -286,13 +291,16 @@ func (s *Store) findResourceListImpl(ctx context.Context, tx *sql.Tx, find *api.
|
|||||||
if v := find.MemoID; v != nil {
|
if v := find.MemoID; v != nil {
|
||||||
where, args = append(where, "resource.id in (SELECT resource_id FROM memo_resource WHERE memo_id = ?)"), append(args, *v)
|
where, args = append(where, "resource.id in (SELECT resource_id FROM memo_resource WHERE memo_id = ?)"), append(args, *v)
|
||||||
}
|
}
|
||||||
|
if v := find.PublicID; v != nil {
|
||||||
|
where, args = append(where, "resource.public_id = ?"), append(args, *v)
|
||||||
|
}
|
||||||
|
|
||||||
fields := []string{"resource.id", "resource.filename", "resource.external_link", "resource.type", "resource.size", "resource.creator_id", "resource.created_ts", "resource.updated_ts"}
|
fields := []string{"resource.id", "resource.filename", "resource.external_link", "resource.type", "resource.size", "resource.creator_id", "resource.created_ts", "resource.updated_ts"}
|
||||||
if find.GetBlob {
|
if find.GetBlob {
|
||||||
fields = append(fields, "resource.blob")
|
fields = append(fields, "resource.blob")
|
||||||
}
|
}
|
||||||
if s.profile.IsDev() {
|
if s.profile.IsDev() {
|
||||||
fields = append(fields, "internal_path")
|
fields = append(fields, "internal_path", "public_id")
|
||||||
}
|
}
|
||||||
|
|
||||||
query := fmt.Sprintf(`
|
query := fmt.Sprintf(`
|
||||||
@ -336,7 +344,7 @@ func (s *Store) findResourceListImpl(ctx context.Context, tx *sql.Tx, find *api.
|
|||||||
dests = append(dests, &resourceRaw.Blob)
|
dests = append(dests, &resourceRaw.Blob)
|
||||||
}
|
}
|
||||||
if s.profile.IsDev() {
|
if s.profile.IsDev() {
|
||||||
dests = append(dests, &resourceRaw.InternalPath)
|
dests = append(dests, &resourceRaw.InternalPath, &resourceRaw.PublicID)
|
||||||
}
|
}
|
||||||
if err := rows.Scan(dests...); err != nil {
|
if err := rows.Scan(dests...); err != nil {
|
||||||
return nil, FormatError(err)
|
return nil, FormatError(err)
|
||||||
|
@ -5,15 +5,7 @@ import ResourceCover from "./ResourceCover";
|
|||||||
import ResourceItemDropdown from "./ResourceItemDropdown";
|
import ResourceItemDropdown from "./ResourceItemDropdown";
|
||||||
import "@/less/resource-card.less";
|
import "@/less/resource-card.less";
|
||||||
|
|
||||||
const ResourceCard = ({
|
const ResourceCard = ({ resource, handleCheckClick, handleUncheckClick }: ResourceItemType) => {
|
||||||
resource,
|
|
||||||
handleCheckClick,
|
|
||||||
handleUncheckClick,
|
|
||||||
handlePreviewBtnClick,
|
|
||||||
handleCopyResourceLinkBtnClick,
|
|
||||||
handleRenameBtnClick,
|
|
||||||
handleDeleteResourceBtnClick,
|
|
||||||
}: ResourceItemType) => {
|
|
||||||
const [isSelected, setIsSelected] = useState<boolean>(false);
|
const [isSelected, setIsSelected] = useState<boolean>(false);
|
||||||
|
|
||||||
const handleSelectBtnClick = () => {
|
const handleSelectBtnClick = () => {
|
||||||
@ -32,13 +24,7 @@ const ResourceCard = ({
|
|||||||
{isSelected ? <Icon.CheckCircle2 className="resource-checkbox !flex" /> : <Icon.Circle className="resource-checkbox" />}
|
{isSelected ? <Icon.CheckCircle2 className="resource-checkbox !flex" /> : <Icon.Circle className="resource-checkbox" />}
|
||||||
</div>
|
</div>
|
||||||
<div className="more-action-btn">
|
<div className="more-action-btn">
|
||||||
<ResourceItemDropdown
|
<ResourceItemDropdown resource={resource} />
|
||||||
resource={resource}
|
|
||||||
handleCopyResourceLinkBtnClick={handleCopyResourceLinkBtnClick}
|
|
||||||
handleDeleteResourceBtnClick={handleDeleteResourceBtnClick}
|
|
||||||
handlePreviewBtnClick={handlePreviewBtnClick}
|
|
||||||
handleRenameBtnClick={handleRenameBtnClick}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full flex flex-row justify-center items-center pb-2 pt-4 px-2">
|
<div className="w-full flex flex-row justify-center items-center pb-2 pt-4 px-2">
|
||||||
|
@ -1,15 +1,8 @@
|
|||||||
|
import { Checkbox } from "@mui/joy";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import ResourceItemDropdown from "./ResourceItemDropdown";
|
import ResourceItemDropdown from "./ResourceItemDropdown";
|
||||||
|
|
||||||
const ResourceItem = ({
|
const ResourceItem = ({ resource, handleCheckClick, handleUncheckClick }: ResourceItemType) => {
|
||||||
resource,
|
|
||||||
handleCheckClick,
|
|
||||||
handleUncheckClick,
|
|
||||||
handlePreviewBtnClick,
|
|
||||||
handleCopyResourceLinkBtnClick,
|
|
||||||
handleRenameBtnClick,
|
|
||||||
handleDeleteResourceBtnClick,
|
|
||||||
}: ResourceItemType) => {
|
|
||||||
const [isSelected, setIsSelected] = useState<boolean>(false);
|
const [isSelected, setIsSelected] = useState<boolean>(false);
|
||||||
|
|
||||||
const handleSelectBtnClick = () => {
|
const handleSelectBtnClick = () => {
|
||||||
@ -23,21 +16,13 @@ const ResourceItem = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={resource.id} className="px-2 py-2 w-full grid grid-cols-10">
|
<div key={resource.id} className="px-2 py-2 w-full grid grid-cols-10">
|
||||||
<span className="w-4 m-auto truncate justify-center">
|
<span className="col-span-1 w-full flex justify-center m-auto truncate ">
|
||||||
<input type="checkbox" onClick={handleSelectBtnClick}></input>
|
<Checkbox checked={isSelected} onChange={handleSelectBtnClick} />
|
||||||
</span>
|
</span>
|
||||||
<span className="w-full m-auto truncate text-base pr-2 last:pr-0 col-span-2">{resource.id}</span>
|
<span className="col-span-2 w-full m-auto truncate text-base pr-2">{resource.id}</span>
|
||||||
<span className="w-full m-auto truncate text-base pr-2 last:pr-0 col-span-6" onClick={() => handleRenameBtnClick(resource)}>
|
<span className="col-span-6 w-full m-auto truncate text-base pr-2">{resource.filename}</span>
|
||||||
{resource.filename}
|
<div className="col-span-1 w-full flex flex-row justify-end items-center pr-2">
|
||||||
</span>
|
<ResourceItemDropdown resource={resource} />
|
||||||
<div className="w-full flex flex-row justify-between items-center mb-2">
|
|
||||||
<ResourceItemDropdown
|
|
||||||
resource={resource}
|
|
||||||
handleCopyResourceLinkBtnClick={handleCopyResourceLinkBtnClick}
|
|
||||||
handleDeleteResourceBtnClick={handleDeleteResourceBtnClick}
|
|
||||||
handlePreviewBtnClick={handlePreviewBtnClick}
|
|
||||||
handleRenameBtnClick={handleRenameBtnClick}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -1,24 +1,77 @@
|
|||||||
|
import copy from "copy-to-clipboard";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import toast from "react-hot-toast";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useResourceStore } from "@/store/module";
|
||||||
|
import { getResourceUrl } from "@/utils/resource";
|
||||||
import Dropdown from "./base/Dropdown";
|
import Dropdown from "./base/Dropdown";
|
||||||
import Icon from "./Icon";
|
import Icon from "./Icon";
|
||||||
|
import { showCommonDialog } from "./Dialog/CommonDialog";
|
||||||
|
import showChangeResourceFilenameDialog from "./ChangeResourceFilenameDialog";
|
||||||
|
import showPreviewImageDialog from "./PreviewImageDialog";
|
||||||
|
|
||||||
interface ResourceItemDropdown {
|
interface Props {
|
||||||
resource: Resource;
|
resource: Resource;
|
||||||
handleRenameBtnClick: (resource: Resource) => void;
|
|
||||||
handleDeleteResourceBtnClick: (resource: Resource) => void;
|
|
||||||
handlePreviewBtnClick: (resource: Resource) => void;
|
|
||||||
handleCopyResourceLinkBtnClick: (resource: Resource) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const ResourceItemDropdown = ({
|
const ResourceItemDropdown = ({ resource }: Props) => {
|
||||||
resource,
|
|
||||||
handlePreviewBtnClick,
|
|
||||||
handleCopyResourceLinkBtnClick,
|
|
||||||
handleRenameBtnClick,
|
|
||||||
handleDeleteResourceBtnClick,
|
|
||||||
}: ResourceItemDropdown) => {
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const resourceStore = useResourceStore();
|
||||||
|
const resources = resourceStore.state.resources;
|
||||||
|
|
||||||
|
const handlePreviewBtnClick = (resource: Resource) => {
|
||||||
|
const resourceUrl = getResourceUrl(resource);
|
||||||
|
if (resource.type.startsWith("image")) {
|
||||||
|
showPreviewImageDialog(
|
||||||
|
resources.filter((r) => r.type.startsWith("image")).map((r) => getResourceUrl(r)),
|
||||||
|
resources.findIndex((r) => r.id === resource.id)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
window.open(resourceUrl);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCopyResourceLinkBtnClick = (resource: Resource) => {
|
||||||
|
const url = getResourceUrl(resource);
|
||||||
|
copy(url);
|
||||||
|
toast.success(t("message.succeed-copy-resource-link"));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleResetResourceLinkBtnClick = (resource: Resource) => {
|
||||||
|
showCommonDialog({
|
||||||
|
title: "Reset resource link",
|
||||||
|
content: "Are you sure to reset the resource link?",
|
||||||
|
style: "warning",
|
||||||
|
dialogName: "reset-resource-link-dialog",
|
||||||
|
onConfirm: async () => {
|
||||||
|
await resourceStore.patchResource({
|
||||||
|
id: resource.id,
|
||||||
|
resetPublicId: true,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRenameBtnClick = (resource: Resource) => {
|
||||||
|
showChangeResourceFilenameDialog(resource.id, resource.filename);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteResourceBtnClick = (resource: Resource) => {
|
||||||
|
let warningText = t("resources.warning-text");
|
||||||
|
if (resource.linkedMemoAmount > 0) {
|
||||||
|
warningText = warningText + `\n${t("resources.linked-amount")}: ${resource.linkedMemoAmount}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
showCommonDialog({
|
||||||
|
title: t("resources.delete-resource"),
|
||||||
|
content: warningText,
|
||||||
|
style: "warning",
|
||||||
|
dialogName: "delete-resource-dialog",
|
||||||
|
onConfirm: async () => {
|
||||||
|
await resourceStore.deleteResourceById(resource.id);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dropdown
|
<Dropdown
|
||||||
@ -38,6 +91,12 @@ const ResourceItemDropdown = ({
|
|||||||
>
|
>
|
||||||
{t("resources.copy-link")}
|
{t("resources.copy-link")}
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
className="w-full text-left text-sm leading-6 py-1 px-3 cursor-pointer rounded hover:bg-gray-100 dark:hover:bg-zinc-600"
|
||||||
|
onClick={() => handleResetResourceLinkBtnClick(resource)}
|
||||||
|
>
|
||||||
|
Reset link
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
className="w-full text-left text-sm leading-6 py-1 px-3 cursor-pointer rounded hover:bg-gray-100 dark:hover:bg-zinc-600"
|
className="w-full text-left text-sm leading-6 py-1 px-3 cursor-pointer rounded hover:bg-gray-100 dark:hover:bg-zinc-600"
|
||||||
onClick={() => handleRenameBtnClick(resource)}
|
onClick={() => handleRenameBtnClick(resource)}
|
||||||
|
@ -1,12 +1,10 @@
|
|||||||
import { Button } from "@mui/joy";
|
import { Button } from "@mui/joy";
|
||||||
import copy from "copy-to-clipboard";
|
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { toast } from "react-hot-toast";
|
import { toast } from "react-hot-toast";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { DEFAULT_MEMO_LIMIT } from "@/helpers/consts";
|
import { DEFAULT_MEMO_LIMIT } from "@/helpers/consts";
|
||||||
import useLoading from "@/hooks/useLoading";
|
import useLoading from "@/hooks/useLoading";
|
||||||
import { useResourceStore } from "@/store/module";
|
import { useResourceStore } from "@/store/module";
|
||||||
import { getResourceUrl } from "@/utils/resource";
|
|
||||||
import Icon from "@/components/Icon";
|
import Icon from "@/components/Icon";
|
||||||
import ResourceCard from "@/components/ResourceCard";
|
import ResourceCard from "@/components/ResourceCard";
|
||||||
import ResourceSearchBar from "@/components/ResourceSearchBar";
|
import ResourceSearchBar from "@/components/ResourceSearchBar";
|
||||||
@ -14,8 +12,6 @@ import MobileHeader from "@/components/MobileHeader";
|
|||||||
import Dropdown from "@/components/base/Dropdown";
|
import Dropdown from "@/components/base/Dropdown";
|
||||||
import ResourceItem from "@/components/ResourceItem";
|
import ResourceItem from "@/components/ResourceItem";
|
||||||
import { showCommonDialog } from "@/components/Dialog/CommonDialog";
|
import { showCommonDialog } from "@/components/Dialog/CommonDialog";
|
||||||
import showChangeResourceFilenameDialog from "@/components/ChangeResourceFilenameDialog";
|
|
||||||
import showPreviewImageDialog from "@/components/PreviewImageDialog";
|
|
||||||
import showCreateResourceDialog from "@/components/CreateResourceDialog";
|
import showCreateResourceDialog from "@/components/CreateResourceDialog";
|
||||||
|
|
||||||
const ResourcesDashboard = () => {
|
const ResourcesDashboard = () => {
|
||||||
@ -24,7 +20,7 @@ const ResourcesDashboard = () => {
|
|||||||
const resourceStore = useResourceStore();
|
const resourceStore = useResourceStore();
|
||||||
const resources = resourceStore.state.resources;
|
const resources = resourceStore.state.resources;
|
||||||
const [selectedList, setSelectedList] = useState<Array<ResourceId>>([]);
|
const [selectedList, setSelectedList] = useState<Array<ResourceId>>([]);
|
||||||
const [listStyle, setListStyle] = useState<"GRID" | "TABLE">("GRID");
|
const [listStyle, setListStyle] = useState<"GRID" | "TABLE">("TABLE");
|
||||||
const [queryText, setQueryText] = useState<string>("");
|
const [queryText, setQueryText] = useState<string>("");
|
||||||
const [dragActive, setDragActive] = useState(false);
|
const [dragActive, setDragActive] = useState(false);
|
||||||
const [isComplete, setIsComplete] = useState<boolean>(false);
|
const [isComplete, setIsComplete] = useState<boolean>(false);
|
||||||
@ -49,7 +45,7 @@ const ResourcesDashboard = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleUncheckBtnClick = (resourceId: ResourceId) => {
|
const handleUncheckBtnClick = (resourceId: ResourceId) => {
|
||||||
setSelectedList(selectedList.filter((resId) => resId !== resourceId));
|
setSelectedList(selectedList.filter((id) => id !== resourceId));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleStyleChangeBtnClick = (listStyle: "GRID" | "TABLE") => {
|
const handleStyleChangeBtnClick = (listStyle: "GRID" | "TABLE") => {
|
||||||
@ -57,27 +53,6 @@ const ResourcesDashboard = () => {
|
|||||||
setSelectedList([]);
|
setSelectedList([]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRenameBtnClick = (resource: Resource) => {
|
|
||||||
showChangeResourceFilenameDialog(resource.id, resource.filename);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDeleteResourceBtnClick = (resource: Resource) => {
|
|
||||||
let warningText = t("resources.warning-text");
|
|
||||||
if (resource.linkedMemoAmount > 0) {
|
|
||||||
warningText = warningText + `\n${t("resources.linked-amount")}: ${resource.linkedMemoAmount}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
showCommonDialog({
|
|
||||||
title: t("resources.delete-resource"),
|
|
||||||
content: warningText,
|
|
||||||
style: "warning",
|
|
||||||
dialogName: "delete-resource-dialog",
|
|
||||||
onConfirm: async () => {
|
|
||||||
await resourceStore.deleteResourceById(resource.id);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDeleteUnusedResourcesBtnClick = async () => {
|
const handleDeleteUnusedResourcesBtnClick = async () => {
|
||||||
let warningText = t("resources.warning-text-unused");
|
let warningText = t("resources.warning-text-unused");
|
||||||
const allResources = await fetchAllResources();
|
const allResources = await fetchAllResources();
|
||||||
@ -125,24 +100,6 @@ const ResourcesDashboard = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePreviewBtnClick = (resource: Resource) => {
|
|
||||||
const resourceUrl = getResourceUrl(resource);
|
|
||||||
if (resource.type.startsWith("image")) {
|
|
||||||
showPreviewImageDialog(
|
|
||||||
resources.filter((r) => r.type.startsWith("image")).map((r) => getResourceUrl(r)),
|
|
||||||
resources.findIndex((r) => r.id === resource.id)
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
window.open(resourceUrl);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCopyResourceLinkBtnClick = (resource: Resource) => {
|
|
||||||
const url = getResourceUrl(resource);
|
|
||||||
copy(url);
|
|
||||||
toast.success(t("message.succeed-copy-resource-link"));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleFetchMoreResourceBtnClick = async () => {
|
const handleFetchMoreResourceBtnClick = async () => {
|
||||||
try {
|
try {
|
||||||
const fetchedResource = await resourceStore.fetchResourceListWithLimit(DEFAULT_MEMO_LIMIT, resources.length);
|
const fetchedResource = await resourceStore.fetchResourceListWithLimit(DEFAULT_MEMO_LIMIT, resources.length);
|
||||||
@ -183,6 +140,16 @@ const ResourcesDashboard = () => {
|
|||||||
setSelectedList([]);
|
setSelectedList([]);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleDrag = (e: React.DragEvent<HTMLDivElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
if (e.type === "dragenter" || e.type === "dragover") {
|
||||||
|
setDragActive(true);
|
||||||
|
} else if (e.type === "dragleave") {
|
||||||
|
setDragActive(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const resourceList = useMemo(
|
const resourceList = useMemo(
|
||||||
() =>
|
() =>
|
||||||
resources
|
resources
|
||||||
@ -194,10 +161,6 @@ const ResourcesDashboard = () => {
|
|||||||
resource={resource}
|
resource={resource}
|
||||||
handleCheckClick={() => handleCheckBtnClick(resource.id)}
|
handleCheckClick={() => handleCheckBtnClick(resource.id)}
|
||||||
handleUncheckClick={() => handleUncheckBtnClick(resource.id)}
|
handleUncheckClick={() => handleUncheckBtnClick(resource.id)}
|
||||||
handleRenameBtnClick={handleRenameBtnClick}
|
|
||||||
handleDeleteResourceBtnClick={handleDeleteResourceBtnClick}
|
|
||||||
handlePreviewBtnClick={handlePreviewBtnClick}
|
|
||||||
handleCopyResourceLinkBtnClick={handleCopyResourceLinkBtnClick}
|
|
||||||
></ResourceItem>
|
></ResourceItem>
|
||||||
) : (
|
) : (
|
||||||
<ResourceCard
|
<ResourceCard
|
||||||
@ -205,26 +168,12 @@ const ResourcesDashboard = () => {
|
|||||||
resource={resource}
|
resource={resource}
|
||||||
handleCheckClick={() => handleCheckBtnClick(resource.id)}
|
handleCheckClick={() => handleCheckBtnClick(resource.id)}
|
||||||
handleUncheckClick={() => handleUncheckBtnClick(resource.id)}
|
handleUncheckClick={() => handleUncheckBtnClick(resource.id)}
|
||||||
handleRenameBtnClick={handleRenameBtnClick}
|
|
||||||
handleDeleteResourceBtnClick={handleDeleteResourceBtnClick}
|
|
||||||
handlePreviewBtnClick={handlePreviewBtnClick}
|
|
||||||
handleCopyResourceLinkBtnClick={handleCopyResourceLinkBtnClick}
|
|
||||||
></ResourceCard>
|
></ResourceCard>
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
[resources, queryText, listStyle]
|
[resources, queryText, listStyle]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleDrag = (e: React.DragEvent<HTMLDivElement>) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
if (e.type === "dragenter" || e.type === "dragover") {
|
|
||||||
setDragActive(true);
|
|
||||||
} else if (e.type === "dragleave") {
|
|
||||||
setDragActive(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDrop = async (e: React.DragEvent<HTMLDivElement>) => {
|
const handleDrop = async (e: React.DragEvent<HTMLDivElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
2
web/src/types/modules/resource.d.ts
vendored
2
web/src/types/modules/resource.d.ts
vendored
@ -10,6 +10,7 @@ interface Resource {
|
|||||||
externalLink: string;
|
externalLink: string;
|
||||||
type: string;
|
type: string;
|
||||||
size: string;
|
size: string;
|
||||||
|
publicId: string;
|
||||||
|
|
||||||
linkedMemoAmount: number;
|
linkedMemoAmount: number;
|
||||||
}
|
}
|
||||||
@ -23,6 +24,7 @@ interface ResourceCreate {
|
|||||||
interface ResourcePatch {
|
interface ResourcePatch {
|
||||||
id: ResourceId;
|
id: ResourceId;
|
||||||
filename?: string;
|
filename?: string;
|
||||||
|
resetPublicId?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ResourceFind {
|
interface ResourceFind {
|
||||||
|
4
web/src/types/resourceItem.d.ts
vendored
4
web/src/types/resourceItem.d.ts
vendored
@ -2,10 +2,6 @@ interface ResourceProps {
|
|||||||
resource: Resource;
|
resource: Resource;
|
||||||
handleCheckClick: () => void;
|
handleCheckClick: () => void;
|
||||||
handleUncheckClick: () => void;
|
handleUncheckClick: () => void;
|
||||||
handleRenameBtnClick: (resource: Resource) => void;
|
|
||||||
handleDeleteResourceBtnClick: (resource: Resource) => void;
|
|
||||||
handlePreviewBtnClick: (resource: Resource) => void;
|
|
||||||
handleCopyResourceLinkBtnClick: (resource: Resource) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type ResourceItemType = ResourceProps;
|
type ResourceItemType = ResourceProps;
|
||||||
|
@ -1,6 +1,3 @@
|
|||||||
export const getResourceUrl = (resource: Resource, withOrigin = true) => {
|
export const getResourceUrl = (resource: Resource, withOrigin = true) => {
|
||||||
if (resource.externalLink) {
|
return `${withOrigin ? window.location.origin : ""}/o/r/${resource.id}/${resource.publicId}`;
|
||||||
return resource.externalLink;
|
|
||||||
}
|
|
||||||
return `${withOrigin ? window.location.origin : ""}/o/r/${resource.id}/${encodeURIComponent(resource.filename)}`;
|
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user