mirror of
https://github.com/usememos/memos.git
synced 2024-11-24 06:35:24 +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"`
|
||||
Type string `json:"type"`
|
||||
Size int64 `json:"size"`
|
||||
PublicID string `json:"publicId"`
|
||||
|
||||
// Related fields
|
||||
LinkedMemoAmount int `json:"linkedMemoAmount"`
|
||||
@ -31,6 +32,7 @@ type ResourceCreate struct {
|
||||
ExternalLink string `json:"externalLink"`
|
||||
Type string `json:"type"`
|
||||
Size int64 `json:"-"`
|
||||
PublicID string `json:"publicId"`
|
||||
}
|
||||
|
||||
type ResourceFind struct {
|
||||
@ -42,6 +44,7 @@ type ResourceFind struct {
|
||||
// Domain specific fields
|
||||
Filename *string `json:"filename"`
|
||||
MemoID *int
|
||||
PublicID *string `json:"publicId"`
|
||||
GetBlob bool
|
||||
|
||||
// Pagination
|
||||
@ -56,7 +59,9 @@ type ResourcePatch struct {
|
||||
UpdatedTs *int64
|
||||
|
||||
// Domain specific fields
|
||||
Filename *string `json:"filename"`
|
||||
Filename *string `json:"filename"`
|
||||
ResetPublicID *bool `json:"resetPublicId"`
|
||||
PublicID *string `json:"-"`
|
||||
}
|
||||
|
||||
type ResourceDelete struct {
|
||||
|
@ -122,6 +122,10 @@ func JWTMiddleware(server *Server, next echo.HandlerFunc, secret string) echo.Ha
|
||||
|
||||
token := findAccessToken(c)
|
||||
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.
|
||||
if common.HasPrefixes(path, "/api/memo") && method == http.MethodGet {
|
||||
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)
|
||||
if err != nil {
|
||||
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))
|
||||
})
|
||||
|
||||
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 {
|
||||
ctx := c.Request().Context()
|
||||
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)
|
||||
}
|
||||
|
||||
if resourcePatch.ResetPublicID != nil && *resourcePatch.ResetPublicID {
|
||||
publicID := common.GenUUID()
|
||||
resourcePatch.PublicID = &publicID
|
||||
}
|
||||
|
||||
resourcePatch.ID = resourceID
|
||||
resource, err = s.Store.PatchResource(ctx, resourcePatch)
|
||||
if err != nil {
|
||||
@ -349,19 +313,19 @@ func (s *Server) registerResourceRoutes(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()
|
||||
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)
|
||||
}
|
||||
filename, err := url.QueryUnescape(c.Param("filename"))
|
||||
publicID, err := url.QueryUnescape(c.Param("publicId"))
|
||||
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{
|
||||
ID: &resourceID,
|
||||
Filename: &filename,
|
||||
PublicID: &publicID,
|
||||
GetBlob: true,
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
if resource.ExternalLink != "" {
|
||||
return c.Redirect(http.StatusSeeOther, resource.ExternalLink)
|
||||
}
|
||||
|
||||
blob := resource.Blob
|
||||
if 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
|
||||
|
||||
secretSessionName := "usememos"
|
||||
embedFrontend(e)
|
||||
|
||||
secret := "usememos"
|
||||
if profile.Mode == "prod" {
|
||||
secretSessionName, err = s.getSystemSecretSessionName(ctx)
|
||||
secret, err = s.getSystemSecretSessionName(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
embedFrontend(e)
|
||||
|
||||
rootGroup := e.Group("")
|
||||
s.registerRSSRoutes(rootGroup)
|
||||
|
||||
publicGroup := e.Group("/o")
|
||||
s.registerResourcePublicRoutes(publicGroup)
|
||||
publicGroup.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return JWTMiddleware(s, next, secret)
|
||||
})
|
||||
registerGetterPublicRoutes(publicGroup)
|
||||
s.registerResourcePublicRoutes(publicGroup)
|
||||
|
||||
apiGroup := e.Group("/api")
|
||||
apiGroup.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return JWTMiddleware(s, next, secretSessionName)
|
||||
return JWTMiddleware(s, next, secret)
|
||||
})
|
||||
s.registerSystemRoutes(apiGroup)
|
||||
s.registerAuthRoutes(apiGroup, secretSessionName)
|
||||
s.registerAuthRoutes(apiGroup, secret)
|
||||
s.registerUserRoutes(apiGroup)
|
||||
s.registerMemoRoutes(apiGroup)
|
||||
s.registerShortcutRoutes(apiGroup)
|
||||
|
@ -74,10 +74,12 @@ CREATE TABLE resource (
|
||||
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
|
||||
filename TEXT NOT NULL DEFAULT '',
|
||||
blob BLOB DEFAULT NULL,
|
||||
internal_path TEXT NOT NULL DEFAULT '',
|
||||
external_link 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
|
||||
|
@ -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
|
||||
Type string
|
||||
Size int64
|
||||
PublicID string
|
||||
LinkedMemoAmount int
|
||||
}
|
||||
|
||||
@ -47,6 +48,7 @@ func (raw *resourceRaw) toResource() *api.Resource {
|
||||
ExternalLink: raw.ExternalLink,
|
||||
Type: raw.Type,
|
||||
Size: raw.Size,
|
||||
PublicID: raw.PublicID,
|
||||
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}
|
||||
placeholders := []string{"?", "?", "?", "?", "?", "?"}
|
||||
if s.profile.IsDev() {
|
||||
fields = append(fields, "internal_path")
|
||||
values = append(values, create.InternalPath)
|
||||
placeholders = append(placeholders, "?")
|
||||
fields = append(fields, "internal_path", "public_id")
|
||||
values = append(values, create.InternalPath, create.PublicID)
|
||||
placeholders = append(placeholders, "?", "?")
|
||||
}
|
||||
|
||||
query := `
|
||||
@ -218,7 +220,7 @@ func (s *Store) createResourceImpl(ctx context.Context, tx *sql.Tx, create *api.
|
||||
&resourceRaw.CreatorID,
|
||||
}
|
||||
if s.profile.IsDev() {
|
||||
dests = append(dests, &resourceRaw.InternalPath)
|
||||
dests = append(dests, &resourceRaw.InternalPath, &resourceRaw.PublicID)
|
||||
}
|
||||
dests = append(dests, []any{&resourceRaw.CreatedTs, &resourceRaw.UpdatedTs}...)
|
||||
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 {
|
||||
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)
|
||||
|
||||
fields := []string{"id", "filename", "external_link", "type", "size", "creator_id", "created_ts", "updated_ts"}
|
||||
if s.profile.IsDev() {
|
||||
fields = append(fields, "internal_path")
|
||||
fields = append(fields, "internal_path", "public_id")
|
||||
}
|
||||
|
||||
query := `
|
||||
@ -262,7 +267,7 @@ func (s *Store) patchResourceImpl(ctx context.Context, tx *sql.Tx, patch *api.Re
|
||||
&resourceRaw.UpdatedTs,
|
||||
}
|
||||
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 {
|
||||
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 {
|
||||
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"}
|
||||
if find.GetBlob {
|
||||
fields = append(fields, "resource.blob")
|
||||
}
|
||||
if s.profile.IsDev() {
|
||||
fields = append(fields, "internal_path")
|
||||
fields = append(fields, "internal_path", "public_id")
|
||||
}
|
||||
|
||||
query := fmt.Sprintf(`
|
||||
@ -336,7 +344,7 @@ func (s *Store) findResourceListImpl(ctx context.Context, tx *sql.Tx, find *api.
|
||||
dests = append(dests, &resourceRaw.Blob)
|
||||
}
|
||||
if s.profile.IsDev() {
|
||||
dests = append(dests, &resourceRaw.InternalPath)
|
||||
dests = append(dests, &resourceRaw.InternalPath, &resourceRaw.PublicID)
|
||||
}
|
||||
if err := rows.Scan(dests...); err != nil {
|
||||
return nil, FormatError(err)
|
||||
|
@ -5,15 +5,7 @@ import ResourceCover from "./ResourceCover";
|
||||
import ResourceItemDropdown from "./ResourceItemDropdown";
|
||||
import "@/less/resource-card.less";
|
||||
|
||||
const ResourceCard = ({
|
||||
resource,
|
||||
handleCheckClick,
|
||||
handleUncheckClick,
|
||||
handlePreviewBtnClick,
|
||||
handleCopyResourceLinkBtnClick,
|
||||
handleRenameBtnClick,
|
||||
handleDeleteResourceBtnClick,
|
||||
}: ResourceItemType) => {
|
||||
const ResourceCard = ({ resource, handleCheckClick, handleUncheckClick }: ResourceItemType) => {
|
||||
const [isSelected, setIsSelected] = useState<boolean>(false);
|
||||
|
||||
const handleSelectBtnClick = () => {
|
||||
@ -32,13 +24,7 @@ const ResourceCard = ({
|
||||
{isSelected ? <Icon.CheckCircle2 className="resource-checkbox !flex" /> : <Icon.Circle className="resource-checkbox" />}
|
||||
</div>
|
||||
<div className="more-action-btn">
|
||||
<ResourceItemDropdown
|
||||
resource={resource}
|
||||
handleCopyResourceLinkBtnClick={handleCopyResourceLinkBtnClick}
|
||||
handleDeleteResourceBtnClick={handleDeleteResourceBtnClick}
|
||||
handlePreviewBtnClick={handlePreviewBtnClick}
|
||||
handleRenameBtnClick={handleRenameBtnClick}
|
||||
/>
|
||||
<ResourceItemDropdown resource={resource} />
|
||||
</div>
|
||||
</div>
|
||||
<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 ResourceItemDropdown from "./ResourceItemDropdown";
|
||||
|
||||
const ResourceItem = ({
|
||||
resource,
|
||||
handleCheckClick,
|
||||
handleUncheckClick,
|
||||
handlePreviewBtnClick,
|
||||
handleCopyResourceLinkBtnClick,
|
||||
handleRenameBtnClick,
|
||||
handleDeleteResourceBtnClick,
|
||||
}: ResourceItemType) => {
|
||||
const ResourceItem = ({ resource, handleCheckClick, handleUncheckClick }: ResourceItemType) => {
|
||||
const [isSelected, setIsSelected] = useState<boolean>(false);
|
||||
|
||||
const handleSelectBtnClick = () => {
|
||||
@ -23,21 +16,13 @@ const ResourceItem = ({
|
||||
|
||||
return (
|
||||
<div key={resource.id} className="px-2 py-2 w-full grid grid-cols-10">
|
||||
<span className="w-4 m-auto truncate justify-center">
|
||||
<input type="checkbox" onClick={handleSelectBtnClick}></input>
|
||||
<span className="col-span-1 w-full flex justify-center m-auto truncate ">
|
||||
<Checkbox checked={isSelected} onChange={handleSelectBtnClick} />
|
||||
</span>
|
||||
<span className="w-full m-auto truncate text-base pr-2 last:pr-0 col-span-2">{resource.id}</span>
|
||||
<span className="w-full m-auto truncate text-base pr-2 last:pr-0 col-span-6" onClick={() => handleRenameBtnClick(resource)}>
|
||||
{resource.filename}
|
||||
</span>
|
||||
<div className="w-full flex flex-row justify-between items-center mb-2">
|
||||
<ResourceItemDropdown
|
||||
resource={resource}
|
||||
handleCopyResourceLinkBtnClick={handleCopyResourceLinkBtnClick}
|
||||
handleDeleteResourceBtnClick={handleDeleteResourceBtnClick}
|
||||
handlePreviewBtnClick={handlePreviewBtnClick}
|
||||
handleRenameBtnClick={handleRenameBtnClick}
|
||||
/>
|
||||
<span className="col-span-2 w-full m-auto truncate text-base pr-2">{resource.id}</span>
|
||||
<span className="col-span-6 w-full m-auto truncate text-base pr-2">{resource.filename}</span>
|
||||
<div className="col-span-1 w-full flex flex-row justify-end items-center pr-2">
|
||||
<ResourceItemDropdown resource={resource} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -1,24 +1,77 @@
|
||||
import copy from "copy-to-clipboard";
|
||||
import React from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useResourceStore } from "@/store/module";
|
||||
import { getResourceUrl } from "@/utils/resource";
|
||||
import Dropdown from "./base/Dropdown";
|
||||
import Icon from "./Icon";
|
||||
import { showCommonDialog } from "./Dialog/CommonDialog";
|
||||
import showChangeResourceFilenameDialog from "./ChangeResourceFilenameDialog";
|
||||
import showPreviewImageDialog from "./PreviewImageDialog";
|
||||
|
||||
interface ResourceItemDropdown {
|
||||
interface Props {
|
||||
resource: Resource;
|
||||
handleRenameBtnClick: (resource: Resource) => void;
|
||||
handleDeleteResourceBtnClick: (resource: Resource) => void;
|
||||
handlePreviewBtnClick: (resource: Resource) => void;
|
||||
handleCopyResourceLinkBtnClick: (resource: Resource) => void;
|
||||
}
|
||||
|
||||
const ResourceItemDropdown = ({
|
||||
resource,
|
||||
handlePreviewBtnClick,
|
||||
handleCopyResourceLinkBtnClick,
|
||||
handleRenameBtnClick,
|
||||
handleDeleteResourceBtnClick,
|
||||
}: ResourceItemDropdown) => {
|
||||
const ResourceItemDropdown = ({ resource }: Props) => {
|
||||
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 (
|
||||
<Dropdown
|
||||
@ -38,6 +91,12 @@ const ResourceItemDropdown = ({
|
||||
>
|
||||
{t("resources.copy-link")}
|
||||
</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
|
||||
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)}
|
||||
|
@ -1,12 +1,10 @@
|
||||
import { Button } from "@mui/joy";
|
||||
import copy from "copy-to-clipboard";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { toast } from "react-hot-toast";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { DEFAULT_MEMO_LIMIT } from "@/helpers/consts";
|
||||
import useLoading from "@/hooks/useLoading";
|
||||
import { useResourceStore } from "@/store/module";
|
||||
import { getResourceUrl } from "@/utils/resource";
|
||||
import Icon from "@/components/Icon";
|
||||
import ResourceCard from "@/components/ResourceCard";
|
||||
import ResourceSearchBar from "@/components/ResourceSearchBar";
|
||||
@ -14,8 +12,6 @@ import MobileHeader from "@/components/MobileHeader";
|
||||
import Dropdown from "@/components/base/Dropdown";
|
||||
import ResourceItem from "@/components/ResourceItem";
|
||||
import { showCommonDialog } from "@/components/Dialog/CommonDialog";
|
||||
import showChangeResourceFilenameDialog from "@/components/ChangeResourceFilenameDialog";
|
||||
import showPreviewImageDialog from "@/components/PreviewImageDialog";
|
||||
import showCreateResourceDialog from "@/components/CreateResourceDialog";
|
||||
|
||||
const ResourcesDashboard = () => {
|
||||
@ -24,7 +20,7 @@ const ResourcesDashboard = () => {
|
||||
const resourceStore = useResourceStore();
|
||||
const resources = resourceStore.state.resources;
|
||||
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 [dragActive, setDragActive] = useState(false);
|
||||
const [isComplete, setIsComplete] = useState<boolean>(false);
|
||||
@ -49,7 +45,7 @@ const ResourcesDashboard = () => {
|
||||
};
|
||||
|
||||
const handleUncheckBtnClick = (resourceId: ResourceId) => {
|
||||
setSelectedList(selectedList.filter((resId) => resId !== resourceId));
|
||||
setSelectedList(selectedList.filter((id) => id !== resourceId));
|
||||
};
|
||||
|
||||
const handleStyleChangeBtnClick = (listStyle: "GRID" | "TABLE") => {
|
||||
@ -57,27 +53,6 @@ const ResourcesDashboard = () => {
|
||||
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 () => {
|
||||
let warningText = t("resources.warning-text-unused");
|
||||
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 () => {
|
||||
try {
|
||||
const fetchedResource = await resourceStore.fetchResourceListWithLimit(DEFAULT_MEMO_LIMIT, resources.length);
|
||||
@ -183,6 +140,16 @@ const ResourcesDashboard = () => {
|
||||
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(
|
||||
() =>
|
||||
resources
|
||||
@ -194,10 +161,6 @@ const ResourcesDashboard = () => {
|
||||
resource={resource}
|
||||
handleCheckClick={() => handleCheckBtnClick(resource.id)}
|
||||
handleUncheckClick={() => handleUncheckBtnClick(resource.id)}
|
||||
handleRenameBtnClick={handleRenameBtnClick}
|
||||
handleDeleteResourceBtnClick={handleDeleteResourceBtnClick}
|
||||
handlePreviewBtnClick={handlePreviewBtnClick}
|
||||
handleCopyResourceLinkBtnClick={handleCopyResourceLinkBtnClick}
|
||||
></ResourceItem>
|
||||
) : (
|
||||
<ResourceCard
|
||||
@ -205,26 +168,12 @@ const ResourcesDashboard = () => {
|
||||
resource={resource}
|
||||
handleCheckClick={() => handleCheckBtnClick(resource.id)}
|
||||
handleUncheckClick={() => handleUncheckBtnClick(resource.id)}
|
||||
handleRenameBtnClick={handleRenameBtnClick}
|
||||
handleDeleteResourceBtnClick={handleDeleteResourceBtnClick}
|
||||
handlePreviewBtnClick={handlePreviewBtnClick}
|
||||
handleCopyResourceLinkBtnClick={handleCopyResourceLinkBtnClick}
|
||||
></ResourceCard>
|
||||
)
|
||||
),
|
||||
[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>) => {
|
||||
e.preventDefault();
|
||||
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;
|
||||
type: string;
|
||||
size: string;
|
||||
publicId: string;
|
||||
|
||||
linkedMemoAmount: number;
|
||||
}
|
||||
@ -23,6 +24,7 @@ interface ResourceCreate {
|
||||
interface ResourcePatch {
|
||||
id: ResourceId;
|
||||
filename?: string;
|
||||
resetPublicId?: boolean;
|
||||
}
|
||||
|
||||
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;
|
||||
handleCheckClick: () => void;
|
||||
handleUncheckClick: () => void;
|
||||
handleRenameBtnClick: (resource: Resource) => void;
|
||||
handleDeleteResourceBtnClick: (resource: Resource) => void;
|
||||
handlePreviewBtnClick: (resource: Resource) => void;
|
||||
handleCopyResourceLinkBtnClick: (resource: Resource) => void;
|
||||
}
|
||||
|
||||
type ResourceItemType = ResourceProps;
|
||||
|
@ -1,6 +1,3 @@
|
||||
export const getResourceUrl = (resource: Resource, withOrigin = true) => {
|
||||
if (resource.externalLink) {
|
||||
return resource.externalLink;
|
||||
}
|
||||
return `${withOrigin ? window.location.origin : ""}/o/r/${resource.id}/${encodeURIComponent(resource.filename)}`;
|
||||
return `${withOrigin ? window.location.origin : ""}/o/r/${resource.id}/${resource.publicId}`;
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user