mirror of
https://github.com/usememos/memos.git
synced 2025-03-28 00:20:13 +01:00
chore: update resource dialog style (#982)
This commit is contained in:
parent
0aaf153717
commit
c5368fe8d3
@ -1,2 +1 @@
|
|||||||
web/node_modules
|
web/node_modules
|
||||||
web/yarn.lock
|
|
||||||
|
@ -9,10 +9,11 @@ type Resource struct {
|
|||||||
UpdatedTs int64 `json:"updatedTs"`
|
UpdatedTs int64 `json:"updatedTs"`
|
||||||
|
|
||||||
// Domain specific fields
|
// Domain specific fields
|
||||||
Filename string `json:"filename"`
|
Filename string `json:"filename"`
|
||||||
Blob []byte `json:"-"`
|
Blob []byte `json:"-"`
|
||||||
Type string `json:"type"`
|
ExternalLink string `json:"externalLink"`
|
||||||
Size int64 `json:"size"`
|
Type string `json:"type"`
|
||||||
|
Size int64 `json:"size"`
|
||||||
|
|
||||||
// Related fields
|
// Related fields
|
||||||
LinkedMemoAmount int `json:"linkedMemoAmount"`
|
LinkedMemoAmount int `json:"linkedMemoAmount"`
|
||||||
@ -23,10 +24,11 @@ type ResourceCreate struct {
|
|||||||
CreatorID int `json:"-"`
|
CreatorID int `json:"-"`
|
||||||
|
|
||||||
// Domain specific fields
|
// Domain specific fields
|
||||||
Filename string `json:"filename"`
|
Filename string `json:"filename"`
|
||||||
Blob []byte `json:"blob"`
|
Blob []byte `json:"-"`
|
||||||
Type string `json:"type"`
|
ExternalLink string `json:"externalLink"`
|
||||||
Size int64 `json:"size"`
|
Type string `json:"-"`
|
||||||
|
Size int64 `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ResourceFind struct {
|
type ResourceFind struct {
|
||||||
|
@ -21,7 +21,7 @@ import (
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
// The max file size is 32MB.
|
// The max file size is 32MB.
|
||||||
maxFileSize = (32 * 8) << 20
|
maxFileSize = 32 << 20
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *Server) registerResourceRoutes(g *echo.Group) {
|
func (s *Server) registerResourceRoutes(g *echo.Group) {
|
||||||
@ -32,6 +32,34 @@ func (s *Server) registerResourceRoutes(g *echo.Group) {
|
|||||||
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
|
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
resourceCreate := &api.ResourceCreate{}
|
||||||
|
if err := json.NewDecoder(c.Request().Body).Decode(resourceCreate); err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post resource request").SetInternal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resourceCreate.CreatorID = userID
|
||||||
|
resource, err := s.Store.CreateResource(ctx, resourceCreate)
|
||||||
|
if err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create resource").SetInternal(err)
|
||||||
|
}
|
||||||
|
if err := s.createResourceCreateActivity(c, resource); err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create activity").SetInternal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
|
||||||
|
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(resource)); err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode resource response").SetInternal(err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
g.POST("/resource/blob", func(c echo.Context) error {
|
||||||
|
ctx := c.Request().Context()
|
||||||
|
userID, ok := c.Get(getUserIDContextKey()).(int)
|
||||||
|
if !ok {
|
||||||
|
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
|
||||||
|
}
|
||||||
|
|
||||||
if err := c.Request().ParseMultipartForm(maxFileSize); err != nil {
|
if err := c.Request().ParseMultipartForm(maxFileSize); err != nil {
|
||||||
return echo.NewHTTPError(http.StatusBadRequest, "Upload file overload max size").SetInternal(err)
|
return echo.NewHTTPError(http.StatusBadRequest, "Upload file overload max size").SetInternal(err)
|
||||||
}
|
}
|
||||||
|
@ -22,10 +22,11 @@ type resourceRaw struct {
|
|||||||
UpdatedTs int64
|
UpdatedTs int64
|
||||||
|
|
||||||
// Domain specific fields
|
// Domain specific fields
|
||||||
Filename string
|
Filename string
|
||||||
Blob []byte
|
Blob []byte
|
||||||
Type string
|
ExternalLink string
|
||||||
Size int64
|
Type string
|
||||||
|
Size int64
|
||||||
}
|
}
|
||||||
|
|
||||||
func (raw *resourceRaw) toResource() *api.Resource {
|
func (raw *resourceRaw) toResource() *api.Resource {
|
||||||
@ -38,10 +39,11 @@ func (raw *resourceRaw) toResource() *api.Resource {
|
|||||||
UpdatedTs: raw.UpdatedTs,
|
UpdatedTs: raw.UpdatedTs,
|
||||||
|
|
||||||
// Domain specific fields
|
// Domain specific fields
|
||||||
Filename: raw.Filename,
|
Filename: raw.Filename,
|
||||||
Blob: raw.Blob,
|
Blob: raw.Blob,
|
||||||
Type: raw.Type,
|
ExternalLink: raw.ExternalLink,
|
||||||
Size: raw.Size,
|
Type: raw.Type,
|
||||||
|
Size: raw.Size,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -215,18 +217,20 @@ func createResource(ctx context.Context, tx *sql.Tx, create *api.ResourceCreate)
|
|||||||
INSERT INTO resource (
|
INSERT INTO resource (
|
||||||
filename,
|
filename,
|
||||||
blob,
|
blob,
|
||||||
|
external_link,
|
||||||
type,
|
type,
|
||||||
size,
|
size,
|
||||||
creator_id
|
creator_id
|
||||||
)
|
)
|
||||||
VALUES (?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?)
|
||||||
RETURNING id, filename, blob, type, size, creator_id, created_ts, updated_ts
|
RETURNING id, filename, blob, external_link, type, size, creator_id, created_ts, updated_ts
|
||||||
`
|
`
|
||||||
var resourceRaw resourceRaw
|
var resourceRaw resourceRaw
|
||||||
if err := tx.QueryRowContext(ctx, query, create.Filename, create.Blob, create.Type, create.Size, create.CreatorID).Scan(
|
if err := tx.QueryRowContext(ctx, query, create.Filename, create.Blob, create.ExternalLink, create.Type, create.Size, create.CreatorID).Scan(
|
||||||
&resourceRaw.ID,
|
&resourceRaw.ID,
|
||||||
&resourceRaw.Filename,
|
&resourceRaw.Filename,
|
||||||
&resourceRaw.Blob,
|
&resourceRaw.Blob,
|
||||||
|
&resourceRaw.ExternalLink,
|
||||||
&resourceRaw.Type,
|
&resourceRaw.Type,
|
||||||
&resourceRaw.Size,
|
&resourceRaw.Size,
|
||||||
&resourceRaw.CreatorID,
|
&resourceRaw.CreatorID,
|
||||||
@ -255,13 +259,14 @@ func patchResource(ctx context.Context, tx *sql.Tx, patch *api.ResourcePatch) (*
|
|||||||
UPDATE resource
|
UPDATE resource
|
||||||
SET ` + strings.Join(set, ", ") + `
|
SET ` + strings.Join(set, ", ") + `
|
||||||
WHERE id = ?
|
WHERE id = ?
|
||||||
RETURNING id, filename, blob, type, size, creator_id, created_ts, updated_ts
|
RETURNING id, filename, blob, external_link, type, size, creator_id, created_ts, updated_ts
|
||||||
`
|
`
|
||||||
var resourceRaw resourceRaw
|
var resourceRaw resourceRaw
|
||||||
if err := tx.QueryRowContext(ctx, query, args...).Scan(
|
if err := tx.QueryRowContext(ctx, query, args...).Scan(
|
||||||
&resourceRaw.ID,
|
&resourceRaw.ID,
|
||||||
&resourceRaw.Filename,
|
&resourceRaw.Filename,
|
||||||
&resourceRaw.Blob,
|
&resourceRaw.Blob,
|
||||||
|
&resourceRaw.ExternalLink,
|
||||||
&resourceRaw.Type,
|
&resourceRaw.Type,
|
||||||
&resourceRaw.Size,
|
&resourceRaw.Size,
|
||||||
&resourceRaw.CreatorID,
|
&resourceRaw.CreatorID,
|
||||||
@ -295,6 +300,7 @@ func findResourceList(ctx context.Context, tx *sql.Tx, find *api.ResourceFind) (
|
|||||||
id,
|
id,
|
||||||
filename,
|
filename,
|
||||||
blob,
|
blob,
|
||||||
|
external_link,
|
||||||
type,
|
type,
|
||||||
size,
|
size,
|
||||||
creator_id,
|
creator_id,
|
||||||
@ -317,6 +323,7 @@ func findResourceList(ctx context.Context, tx *sql.Tx, find *api.ResourceFind) (
|
|||||||
&resourceRaw.ID,
|
&resourceRaw.ID,
|
||||||
&resourceRaw.Filename,
|
&resourceRaw.Filename,
|
||||||
&resourceRaw.Blob,
|
&resourceRaw.Blob,
|
||||||
|
&resourceRaw.ExternalLink,
|
||||||
&resourceRaw.Type,
|
&resourceRaw.Type,
|
||||||
&resourceRaw.Size,
|
&resourceRaw.Size,
|
||||||
&resourceRaw.CreatorID,
|
&resourceRaw.CreatorID,
|
||||||
|
@ -90,6 +90,7 @@ const CreateTagDialog: React.FC<Props> = (props: Props) => {
|
|||||||
<div className="dialog-content-container !w-80">
|
<div className="dialog-content-container !w-80">
|
||||||
<Input
|
<Input
|
||||||
className="mb-2"
|
className="mb-2"
|
||||||
|
size="md"
|
||||||
placeholder="TAG_NAME"
|
placeholder="TAG_NAME"
|
||||||
value={tagName}
|
value={tagName}
|
||||||
onChange={handleTagNameChanged}
|
onChange={handleTagNameChanged}
|
||||||
|
@ -9,8 +9,8 @@ import theme from "../../theme";
|
|||||||
import "../../less/base-dialog.less";
|
import "../../less/base-dialog.less";
|
||||||
|
|
||||||
interface DialogConfig {
|
interface DialogConfig {
|
||||||
className: string;
|
|
||||||
dialogName: string;
|
dialogName: string;
|
||||||
|
className?: string;
|
||||||
clickSpaceDestroy?: boolean;
|
clickSpaceDestroy?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -55,7 +55,7 @@ const BaseDialog: React.FC<Props> = (props: Props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`dialog-wrapper ${className}`} onMouseDown={handleSpaceClicked}>
|
<div className={`dialog-wrapper ${className ?? ""}`} onMouseDown={handleSpaceClicked}>
|
||||||
<div ref={dialogContainerRef} className="dialog-container" onMouseDown={(e) => e.stopPropagation()}>
|
<div ref={dialogContainerRef} className="dialog-container" onMouseDown={(e) => e.stopPropagation()}>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
|
@ -37,6 +37,7 @@ const setEditingMemoVisibilityCache = (visibility: Visibility) => {
|
|||||||
interface State {
|
interface State {
|
||||||
fullscreen: boolean;
|
fullscreen: boolean;
|
||||||
isUploadingResource: boolean;
|
isUploadingResource: boolean;
|
||||||
|
isRequesting: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MemoEditor = () => {
|
const MemoEditor = () => {
|
||||||
@ -51,6 +52,7 @@ const MemoEditor = () => {
|
|||||||
const [state, setState] = useState<State>({
|
const [state, setState] = useState<State>({
|
||||||
isUploadingResource: false,
|
isUploadingResource: false,
|
||||||
fullscreen: false,
|
fullscreen: false,
|
||||||
|
isRequesting: false,
|
||||||
});
|
});
|
||||||
const [allowSave, setAllowSave] = useState<boolean>(false);
|
const [allowSave, setAllowSave] = useState<boolean>(false);
|
||||||
const editorState = editorStore.state;
|
const editorState = editorStore.state;
|
||||||
@ -280,7 +282,7 @@ const MemoEditor = () => {
|
|||||||
let resource = undefined;
|
let resource = undefined;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
resource = await resourceStore.upload(file);
|
resource = await resourceStore.createResourceWithBlob(file);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
toastHelper.error(error.response.data.message);
|
toastHelper.error(error.response.data.message);
|
||||||
@ -305,6 +307,16 @@ const MemoEditor = () => {
|
|||||||
}, [editorState.editMemoId]);
|
}, [editorState.editMemoId]);
|
||||||
|
|
||||||
const handleSaveBtnClick = async () => {
|
const handleSaveBtnClick = async () => {
|
||||||
|
if (state.isRequesting) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setState((state) => {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
isRequesting: true,
|
||||||
|
};
|
||||||
|
});
|
||||||
const content = editorRef.current?.getContent() ?? "";
|
const content = editorRef.current?.getContent() ?? "";
|
||||||
try {
|
try {
|
||||||
const { editMemoId } = editorStore.getState();
|
const { editMemoId } = editorStore.getState();
|
||||||
@ -332,6 +344,12 @@ const MemoEditor = () => {
|
|||||||
console.error(error);
|
console.error(error);
|
||||||
toastHelper.error(error.response.data.message);
|
toastHelper.error(error.response.data.message);
|
||||||
}
|
}
|
||||||
|
setState((state) => {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
isRequesting: false,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
// Upsert tag with the content.
|
// Upsert tag with the content.
|
||||||
const matchedNodes = getMatchedNodes(content);
|
const matchedNodes = getMatchedNodes(content);
|
||||||
@ -561,7 +579,7 @@ const MemoEditor = () => {
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className="action-btn confirm-btn"
|
className="action-btn confirm-btn"
|
||||||
disabled={!(allowSave || editorState.resourceList.length > 0) || state.isUploadingResource}
|
disabled={!(allowSave || editorState.resourceList.length > 0) || state.isUploadingResource || state.isRequesting}
|
||||||
onClick={handleSaveBtnClick}
|
onClick={handleSaveBtnClick}
|
||||||
>
|
>
|
||||||
{t("editor.save")}
|
{t("editor.save")}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { Tooltip } from "@mui/joy";
|
import { Button } from "@mui/joy";
|
||||||
import copy from "copy-to-clipboard";
|
import copy from "copy-to-clipboard";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import useLoading from "../hooks/useLoading";
|
import useLoading from "../hooks/useLoading";
|
||||||
import { useResourceStore } from "../store/module";
|
import { useResourceStore } from "../store/module";
|
||||||
@ -16,19 +16,12 @@ import "../less/resources-dialog.less";
|
|||||||
|
|
||||||
type Props = DialogProps;
|
type Props = DialogProps;
|
||||||
|
|
||||||
interface State {
|
|
||||||
isUploadingResource: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ResourcesDialog: React.FC<Props> = (props: Props) => {
|
const ResourcesDialog: React.FC<Props> = (props: Props) => {
|
||||||
const { destroy } = props;
|
const { destroy } = props;
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const loadingState = useLoading();
|
const loadingState = useLoading();
|
||||||
const resourceStore = useResourceStore();
|
const resourceStore = useResourceStore();
|
||||||
const resources = resourceStore.state.resources;
|
const resources = resourceStore.state.resources;
|
||||||
const [state, setState] = useState<State>({
|
|
||||||
isUploadingResource: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
resourceStore
|
resourceStore
|
||||||
@ -43,10 +36,6 @@ const ResourcesDialog: React.FC<Props> = (props: Props) => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleUploadFileBtnClick = async () => {
|
const handleUploadFileBtnClick = async () => {
|
||||||
if (state.isUploadingResource) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const inputEl = document.createElement("input");
|
const inputEl = document.createElement("input");
|
||||||
inputEl.style.position = "fixed";
|
inputEl.style.position = "fixed";
|
||||||
inputEl.style.top = "-100vh";
|
inputEl.style.top = "-100vh";
|
||||||
@ -60,22 +49,12 @@ const ResourcesDialog: React.FC<Props> = (props: Props) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setState({
|
|
||||||
...state,
|
|
||||||
isUploadingResource: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const file of inputEl.files) {
|
for (const file of inputEl.files) {
|
||||||
try {
|
try {
|
||||||
await resourceStore.upload(file);
|
await resourceStore.createResourceWithBlob(file);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
toastHelper.error(error.response.data.message);
|
toastHelper.error(error.response.data.message);
|
||||||
} finally {
|
|
||||||
setState({
|
|
||||||
...state,
|
|
||||||
isUploadingResource: false,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -158,18 +137,16 @@ const ResourcesDialog: React.FC<Props> = (props: Props) => {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="dialog-content-container">
|
<div className="dialog-content-container">
|
||||||
<div className="action-buttons-container">
|
<div className="w-full flex flex-row justify-between items-center">
|
||||||
<div className="buttons-wrapper">
|
<div className="flex flex-row justify-start items-center space-x-2">
|
||||||
<div className="upload-resource-btn" onClick={() => handleUploadFileBtnClick()}>
|
<Button onClick={() => handleUploadFileBtnClick()} startDecorator={<Icon.Plus className="w-5 h-auto" />}>
|
||||||
<Icon.File className="icon-img" />
|
{t("common.create")}
|
||||||
<span>{t("resources.upload")}</span>
|
</Button>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="buttons-wrapper">
|
<div className="flex flex-row justify-end items-center">
|
||||||
<div className="delete-unused-resource-btn" onClick={handleDeleteUnusedResourcesBtnClick}>
|
<Button color="danger" onClick={handleDeleteUnusedResourcesBtnClick} startDecorator={<Icon.Trash2 className="w-4 h-auto" />}>
|
||||||
<Icon.Trash2 className="icon-img" />
|
<span>{t("resources.clear")}</span>
|
||||||
<span>{t("resources.clear-unused-resources")}</span>
|
</Button>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{loadingState.isLoading ? (
|
{loadingState.isLoading ? (
|
||||||
@ -189,9 +166,9 @@ const ResourcesDialog: React.FC<Props> = (props: Props) => {
|
|||||||
resources.map((resource) => (
|
resources.map((resource) => (
|
||||||
<div key={resource.id} className="resource-container">
|
<div key={resource.id} className="resource-container">
|
||||||
<span className="field-text id-text">{resource.id}</span>
|
<span className="field-text id-text">{resource.id}</span>
|
||||||
<Tooltip title={resource.filename}>
|
<span className="field-text name-text" onClick={() => handleRenameBtnClick(resource)}>
|
||||||
<span className="field-text name-text">{resource.filename}</span>
|
{resource.filename}
|
||||||
</Tooltip>
|
</span>
|
||||||
<div className="buttons-container">
|
<div className="buttons-container">
|
||||||
<Dropdown
|
<Dropdown
|
||||||
actionsClassName="!w-28"
|
actionsClassName="!w-28"
|
||||||
@ -203,12 +180,6 @@ const ResourcesDialog: React.FC<Props> = (props: Props) => {
|
|||||||
>
|
>
|
||||||
{t("resources.preview")}
|
{t("resources.preview")}
|
||||||
</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={() => handleRenameBtnClick(resource)}
|
|
||||||
>
|
|
||||||
{t("resources.rename")}
|
|
||||||
</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={() => handleCopyResourceLinkBtnClick(resource)}
|
onClick={() => handleCopyResourceLinkBtnClick(resource)}
|
||||||
|
@ -4,6 +4,7 @@ import { Link } from "react-router-dom";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useLocationStore, useUserStore } from "../store/module";
|
import { useLocationStore, useUserStore } from "../store/module";
|
||||||
import showDailyReviewDialog from "./DailyReviewDialog";
|
import showDailyReviewDialog from "./DailyReviewDialog";
|
||||||
|
import showResourcesDialog from "./ResourcesDialog";
|
||||||
import showSettingDialog from "./SettingDialog";
|
import showSettingDialog from "./SettingDialog";
|
||||||
import UserBanner from "./UserBanner";
|
import UserBanner from "./UserBanner";
|
||||||
import UsageHeatMap from "./UsageHeatMap";
|
import UsageHeatMap from "./UsageHeatMap";
|
||||||
@ -38,6 +39,9 @@ const Sidebar = () => {
|
|||||||
<Link to="/explore" className="btn action-btn">
|
<Link to="/explore" className="btn action-btn">
|
||||||
<span className="icon">🏂</span> {t("common.explore")}
|
<span className="icon">🏂</span> {t("common.explore")}
|
||||||
</Link>
|
</Link>
|
||||||
|
<button className="btn action-btn" onClick={() => showResourcesDialog()}>
|
||||||
|
<span className="icon">🗂️</span> {t("sidebar.resources")}
|
||||||
|
</button>
|
||||||
{!userStore.isVisitorMode() && (
|
{!userStore.isVisitorMode() && (
|
||||||
<>
|
<>
|
||||||
<button className="btn action-btn" onClick={handleSettingBtnClick}>
|
<button className="btn action-btn" onClick={handleSettingBtnClick}>
|
||||||
|
@ -5,7 +5,6 @@ import { getMemoStats } from "../helpers/api";
|
|||||||
import * as utils from "../helpers/utils";
|
import * as utils from "../helpers/utils";
|
||||||
import Icon from "./Icon";
|
import Icon from "./Icon";
|
||||||
import Dropdown from "./common/Dropdown";
|
import Dropdown from "./common/Dropdown";
|
||||||
import showResourcesDialog from "./ResourcesDialog";
|
|
||||||
import showArchivedMemoDialog from "./ArchivedMemoDialog";
|
import showArchivedMemoDialog from "./ArchivedMemoDialog";
|
||||||
import showAboutSiteDialog from "./AboutSiteDialog";
|
import showAboutSiteDialog from "./AboutSiteDialog";
|
||||||
import "../less/user-banner.less";
|
import "../less/user-banner.less";
|
||||||
@ -51,10 +50,6 @@ const UserBanner = () => {
|
|||||||
locationStore.clearQuery();
|
locationStore.clearQuery();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleResourcesBtnClick = () => {
|
|
||||||
showResourcesDialog();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleArchivedBtnClick = () => {
|
const handleArchivedBtnClick = () => {
|
||||||
showArchivedMemoDialog();
|
showArchivedMemoDialog();
|
||||||
};
|
};
|
||||||
@ -82,17 +77,11 @@ const UserBanner = () => {
|
|||||||
<>
|
<>
|
||||||
{!userStore.isVisitorMode() && (
|
{!userStore.isVisitorMode() && (
|
||||||
<>
|
<>
|
||||||
<button
|
|
||||||
className="w-full px-3 whitespace-nowrap text-left leading-10 cursor-pointer rounded dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-zinc-800"
|
|
||||||
onClick={handleResourcesBtnClick}
|
|
||||||
>
|
|
||||||
<span className="mr-1">🌄</span> {t("sidebar.resources")}
|
|
||||||
</button>
|
|
||||||
<button
|
<button
|
||||||
className="w-full px-3 whitespace-nowrap text-left leading-10 cursor-pointer rounded dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-zinc-800"
|
className="w-full px-3 whitespace-nowrap text-left leading-10 cursor-pointer rounded dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-zinc-800"
|
||||||
onClick={handleArchivedBtnClick}
|
onClick={handleArchivedBtnClick}
|
||||||
>
|
>
|
||||||
<span className="mr-1">🗂</span> {t("sidebar.archived")}
|
<span className="mr-1">🗃️</span> {t("sidebar.archived")}
|
||||||
</button>
|
</button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
@ -153,8 +153,12 @@ export function getResourceList() {
|
|||||||
return axios.get<ResponseObject<Resource[]>>("/api/resource");
|
return axios.get<ResponseObject<Resource[]>>("/api/resource");
|
||||||
}
|
}
|
||||||
|
|
||||||
export function uploadFile(formData: FormData) {
|
export function createResource(resourceCreate: ResourceCreate) {
|
||||||
return axios.post<ResponseObject<Resource>>("/api/resource", formData);
|
return axios.post<ResponseObject<Resource>>("/api/resource", resourceCreate);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createResourceWithBlob(formData: FormData) {
|
||||||
|
return axios.post<ResponseObject<Resource>>("/api/resource/blob", formData);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deleteResourceById(id: ResourceId) {
|
export function deleteResourceById(id: ResourceId) {
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
@apply relative w-full h-auto mx-auto flex flex-row justify-start sm:justify-center items-start;
|
@apply relative w-full h-auto mx-auto flex flex-row justify-start sm:justify-center items-start;
|
||||||
|
|
||||||
> .sidebar-wrapper {
|
> .sidebar-wrapper {
|
||||||
@apply flex-shrink-0 ml-calc;
|
@apply flex-shrink-0 h-full ml-calc;
|
||||||
}
|
}
|
||||||
|
|
||||||
> .memos-wrapper {
|
> .memos-wrapper {
|
||||||
|
@ -62,11 +62,11 @@
|
|||||||
@apply w-full truncate text-base pr-2 last:pr-0;
|
@apply w-full truncate text-base pr-2 last:pr-0;
|
||||||
|
|
||||||
&.id-text {
|
&.id-text {
|
||||||
@apply col-span-2;
|
@apply col-span-1;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.name-text {
|
&.name-text {
|
||||||
@apply col-span-4;
|
@apply col-span-5;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -75,7 +75,7 @@
|
|||||||
"warning-text": "Are you sure to delete this resource? THIS ACTION IS IRREVERSIABLE❗",
|
"warning-text": "Are you sure to delete this resource? THIS ACTION IS IRREVERSIABLE❗",
|
||||||
"linked-amount": "Linked memo amount",
|
"linked-amount": "Linked memo amount",
|
||||||
"rename": "Rename",
|
"rename": "Rename",
|
||||||
"clear-unused-resources": "Clear unused resources",
|
"clear": "Clear",
|
||||||
"warning-text-unused": "Are you sure to delete these unused resource? THIS ACTION IS IRREVERSIABLE❗",
|
"warning-text-unused": "Are you sure to delete these unused resource? THIS ACTION IS IRREVERSIABLE❗",
|
||||||
"no-unused-resources": "No unused resources",
|
"no-unused-resources": "No unused resources",
|
||||||
"name": "Name"
|
"name": "Name"
|
||||||
|
@ -75,7 +75,7 @@
|
|||||||
"warning-text": "确定删除这个资源么?此操作不可逆❗",
|
"warning-text": "确定删除这个资源么?此操作不可逆❗",
|
||||||
"linked-amount": "链接的 Memo 数量",
|
"linked-amount": "链接的 Memo 数量",
|
||||||
"rename": "重命名",
|
"rename": "重命名",
|
||||||
"clear-unused-resources": "清除无用资源",
|
"clear": "清除",
|
||||||
"warning-text-unused": "确定删除这些无用资源么?此操作不可逆❗",
|
"warning-text-unused": "确定删除这些无用资源么?此操作不可逆❗",
|
||||||
"no-unused-resources": "无可删除的资源",
|
"no-unused-resources": "无可删除的资源",
|
||||||
"name": "资源名称"
|
"name": "资源名称"
|
||||||
|
@ -2,6 +2,8 @@ import store, { useAppSelector } from "../";
|
|||||||
import { patchResource, setResources, deleteResource } from "../reducer/resource";
|
import { patchResource, setResources, deleteResource } from "../reducer/resource";
|
||||||
import * as api from "../../helpers/api";
|
import * as api from "../../helpers/api";
|
||||||
|
|
||||||
|
const MAX_FILE_SIZE = 32 << 20;
|
||||||
|
|
||||||
const convertResponseModelResource = (resource: Resource): Resource => {
|
const convertResponseModelResource = (resource: Resource): Resource => {
|
||||||
return {
|
return {
|
||||||
...resource,
|
...resource,
|
||||||
@ -24,16 +26,22 @@ export const useResourceStore = () => {
|
|||||||
store.dispatch(setResources(resourceList));
|
store.dispatch(setResources(resourceList));
|
||||||
return resourceList;
|
return resourceList;
|
||||||
},
|
},
|
||||||
async upload(file: File): Promise<Resource> {
|
async createResource(resourceCreate: ResourceCreate): Promise<Resource> {
|
||||||
|
const { data } = (await api.createResource(resourceCreate)).data;
|
||||||
|
const resource = convertResponseModelResource(data);
|
||||||
|
const resourceList = state.resources;
|
||||||
|
store.dispatch(setResources([resource, ...resourceList]));
|
||||||
|
return resource;
|
||||||
|
},
|
||||||
|
async createResourceWithBlob(file: File): Promise<Resource> {
|
||||||
const { name: filename, size } = file;
|
const { name: filename, size } = file;
|
||||||
|
if (size > MAX_FILE_SIZE) {
|
||||||
if (size > 64 << 20) {
|
return Promise.reject("overload max size: 32MB");
|
||||||
return Promise.reject("overload max size: 8MB");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append("file", file, filename);
|
formData.append("file", file, filename);
|
||||||
const { data } = (await api.uploadFile(formData)).data;
|
const { data } = (await api.createResourceWithBlob(formData)).data;
|
||||||
const resource = convertResponseModelResource(data);
|
const resource = convertResponseModelResource(data);
|
||||||
const resourceList = state.resources;
|
const resourceList = state.resources;
|
||||||
store.dispatch(setResources([resource, ...resourceList]));
|
store.dispatch(setResources([resource, ...resourceList]));
|
||||||
|
@ -7,6 +7,11 @@ const theme = extendTheme({
|
|||||||
size: "sm",
|
size: "sm",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
JoyInput: {
|
||||||
|
defaultProps: {
|
||||||
|
size: "sm",
|
||||||
|
},
|
||||||
|
},
|
||||||
JoySelect: {
|
JoySelect: {
|
||||||
defaultProps: {
|
defaultProps: {
|
||||||
size: "sm",
|
size: "sm",
|
||||||
|
6
web/src/types/modules/resource.d.ts
vendored
6
web/src/types/modules/resource.d.ts
vendored
@ -7,12 +7,18 @@ interface Resource {
|
|||||||
updatedTs: TimeStamp;
|
updatedTs: TimeStamp;
|
||||||
|
|
||||||
filename: string;
|
filename: string;
|
||||||
|
externalLink: string;
|
||||||
type: string;
|
type: string;
|
||||||
size: string;
|
size: string;
|
||||||
|
|
||||||
linkedMemoAmount: number;
|
linkedMemoAmount: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ResourceCreate {
|
||||||
|
filename: string;
|
||||||
|
externalLink: string;
|
||||||
|
}
|
||||||
|
|
||||||
interface ResourcePatch {
|
interface ResourcePatch {
|
||||||
id: ResourceId;
|
id: ResourceId;
|
||||||
filename?: string;
|
filename?: string;
|
||||||
|
@ -1,3 +1,6 @@
|
|||||||
export const getResourceUrl = (resource: Resource, withOrigin = true) => {
|
export const getResourceUrl = (resource: Resource, withOrigin = true) => {
|
||||||
|
if (resource.externalLink) {
|
||||||
|
return resource.externalLink;
|
||||||
|
}
|
||||||
return `${withOrigin ? window.location.origin : ""}/o/r/${resource.id}/${encodeURI(resource.filename)}`;
|
return `${withOrigin ? window.location.origin : ""}/o/r/${resource.id}/${encodeURI(resource.filename)}`;
|
||||||
};
|
};
|
||||||
|
Loading…
x
Reference in New Issue
Block a user