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:
boojack 2023-04-03 13:41:27 +08:00 committed by GitHub
parent c9a5df81ce
commit 1cab30f32f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 179 additions and 190 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,4 @@
ALTER TABLE
resource
ADD
COLUMN internal_path TEXT NOT NULL DEFAULT '';

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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