chore: update resource dialog style (#982)

This commit is contained in:
boojack 2023-01-21 08:46:49 +08:00 committed by GitHub
parent 0aaf153717
commit c5368fe8d3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 139 additions and 94 deletions

View file

@ -1,2 +1 @@
web/node_modules
web/yarn.lock

View file

@ -9,10 +9,11 @@ type Resource struct {
UpdatedTs int64 `json:"updatedTs"`
// Domain specific fields
Filename string `json:"filename"`
Blob []byte `json:"-"`
Type string `json:"type"`
Size int64 `json:"size"`
Filename string `json:"filename"`
Blob []byte `json:"-"`
ExternalLink string `json:"externalLink"`
Type string `json:"type"`
Size int64 `json:"size"`
// Related fields
LinkedMemoAmount int `json:"linkedMemoAmount"`
@ -23,10 +24,11 @@ type ResourceCreate struct {
CreatorID int `json:"-"`
// Domain specific fields
Filename string `json:"filename"`
Blob []byte `json:"blob"`
Type string `json:"type"`
Size int64 `json:"size"`
Filename string `json:"filename"`
Blob []byte `json:"-"`
ExternalLink string `json:"externalLink"`
Type string `json:"-"`
Size int64 `json:"-"`
}
type ResourceFind struct {

View file

@ -21,7 +21,7 @@ import (
const (
// The max file size is 32MB.
maxFileSize = (32 * 8) << 20
maxFileSize = 32 << 20
)
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")
}
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 {
return echo.NewHTTPError(http.StatusBadRequest, "Upload file overload max size").SetInternal(err)
}

View file

@ -22,10 +22,11 @@ type resourceRaw struct {
UpdatedTs int64
// Domain specific fields
Filename string
Blob []byte
Type string
Size int64
Filename string
Blob []byte
ExternalLink string
Type string
Size int64
}
func (raw *resourceRaw) toResource() *api.Resource {
@ -38,10 +39,11 @@ func (raw *resourceRaw) toResource() *api.Resource {
UpdatedTs: raw.UpdatedTs,
// Domain specific fields
Filename: raw.Filename,
Blob: raw.Blob,
Type: raw.Type,
Size: raw.Size,
Filename: raw.Filename,
Blob: raw.Blob,
ExternalLink: raw.ExternalLink,
Type: raw.Type,
Size: raw.Size,
}
}
@ -215,18 +217,20 @@ func createResource(ctx context.Context, tx *sql.Tx, create *api.ResourceCreate)
INSERT INTO resource (
filename,
blob,
external_link,
type,
size,
creator_id
)
VALUES (?, ?, ?, ?, ?)
RETURNING id, filename, blob, type, size, creator_id, created_ts, updated_ts
VALUES (?, ?, ?, ?, ?, ?)
RETURNING id, filename, blob, external_link, type, size, creator_id, created_ts, updated_ts
`
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.Filename,
&resourceRaw.Blob,
&resourceRaw.ExternalLink,
&resourceRaw.Type,
&resourceRaw.Size,
&resourceRaw.CreatorID,
@ -255,13 +259,14 @@ func patchResource(ctx context.Context, tx *sql.Tx, patch *api.ResourcePatch) (*
UPDATE resource
SET ` + strings.Join(set, ", ") + `
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
if err := tx.QueryRowContext(ctx, query, args...).Scan(
&resourceRaw.ID,
&resourceRaw.Filename,
&resourceRaw.Blob,
&resourceRaw.ExternalLink,
&resourceRaw.Type,
&resourceRaw.Size,
&resourceRaw.CreatorID,
@ -295,6 +300,7 @@ func findResourceList(ctx context.Context, tx *sql.Tx, find *api.ResourceFind) (
id,
filename,
blob,
external_link,
type,
size,
creator_id,
@ -317,6 +323,7 @@ func findResourceList(ctx context.Context, tx *sql.Tx, find *api.ResourceFind) (
&resourceRaw.ID,
&resourceRaw.Filename,
&resourceRaw.Blob,
&resourceRaw.ExternalLink,
&resourceRaw.Type,
&resourceRaw.Size,
&resourceRaw.CreatorID,

View file

@ -90,6 +90,7 @@ const CreateTagDialog: React.FC<Props> = (props: Props) => {
<div className="dialog-content-container !w-80">
<Input
className="mb-2"
size="md"
placeholder="TAG_NAME"
value={tagName}
onChange={handleTagNameChanged}

View file

@ -9,8 +9,8 @@ import theme from "../../theme";
import "../../less/base-dialog.less";
interface DialogConfig {
className: string;
dialogName: string;
className?: string;
clickSpaceDestroy?: boolean;
}
@ -55,7 +55,7 @@ const BaseDialog: React.FC<Props> = (props: Props) => {
};
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()}>
{children}
</div>

View file

@ -37,6 +37,7 @@ const setEditingMemoVisibilityCache = (visibility: Visibility) => {
interface State {
fullscreen: boolean;
isUploadingResource: boolean;
isRequesting: boolean;
}
const MemoEditor = () => {
@ -51,6 +52,7 @@ const MemoEditor = () => {
const [state, setState] = useState<State>({
isUploadingResource: false,
fullscreen: false,
isRequesting: false,
});
const [allowSave, setAllowSave] = useState<boolean>(false);
const editorState = editorStore.state;
@ -280,7 +282,7 @@ const MemoEditor = () => {
let resource = undefined;
try {
resource = await resourceStore.upload(file);
resource = await resourceStore.createResourceWithBlob(file);
} catch (error: any) {
console.error(error);
toastHelper.error(error.response.data.message);
@ -305,6 +307,16 @@ const MemoEditor = () => {
}, [editorState.editMemoId]);
const handleSaveBtnClick = async () => {
if (state.isRequesting) {
return;
}
setState((state) => {
return {
...state,
isRequesting: true,
};
});
const content = editorRef.current?.getContent() ?? "";
try {
const { editMemoId } = editorStore.getState();
@ -332,6 +344,12 @@ const MemoEditor = () => {
console.error(error);
toastHelper.error(error.response.data.message);
}
setState((state) => {
return {
...state,
isRequesting: false,
};
});
// Upsert tag with the content.
const matchedNodes = getMatchedNodes(content);
@ -561,7 +579,7 @@ const MemoEditor = () => {
</button>
<button
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}
>
{t("editor.save")}

View file

@ -1,6 +1,6 @@
import { Tooltip } from "@mui/joy";
import { Button } from "@mui/joy";
import copy from "copy-to-clipboard";
import { useEffect, useState } from "react";
import { useEffect } from "react";
import { useTranslation } from "react-i18next";
import useLoading from "../hooks/useLoading";
import { useResourceStore } from "../store/module";
@ -16,19 +16,12 @@ import "../less/resources-dialog.less";
type Props = DialogProps;
interface State {
isUploadingResource: boolean;
}
const ResourcesDialog: React.FC<Props> = (props: Props) => {
const { destroy } = props;
const { t } = useTranslation();
const loadingState = useLoading();
const resourceStore = useResourceStore();
const resources = resourceStore.state.resources;
const [state, setState] = useState<State>({
isUploadingResource: false,
});
useEffect(() => {
resourceStore
@ -43,10 +36,6 @@ const ResourcesDialog: React.FC<Props> = (props: Props) => {
}, []);
const handleUploadFileBtnClick = async () => {
if (state.isUploadingResource) {
return;
}
const inputEl = document.createElement("input");
inputEl.style.position = "fixed";
inputEl.style.top = "-100vh";
@ -60,22 +49,12 @@ const ResourcesDialog: React.FC<Props> = (props: Props) => {
return;
}
setState({
...state,
isUploadingResource: true,
});
for (const file of inputEl.files) {
try {
await resourceStore.upload(file);
await resourceStore.createResourceWithBlob(file);
} catch (error: any) {
console.error(error);
toastHelper.error(error.response.data.message);
} finally {
setState({
...state,
isUploadingResource: false,
});
}
}
@ -158,18 +137,16 @@ const ResourcesDialog: React.FC<Props> = (props: Props) => {
</button>
</div>
<div className="dialog-content-container">
<div className="action-buttons-container">
<div className="buttons-wrapper">
<div className="upload-resource-btn" onClick={() => handleUploadFileBtnClick()}>
<Icon.File className="icon-img" />
<span>{t("resources.upload")}</span>
</div>
<div className="w-full flex flex-row justify-between items-center">
<div className="flex flex-row justify-start items-center space-x-2">
<Button onClick={() => handleUploadFileBtnClick()} startDecorator={<Icon.Plus className="w-5 h-auto" />}>
{t("common.create")}
</Button>
</div>
<div className="buttons-wrapper">
<div className="delete-unused-resource-btn" onClick={handleDeleteUnusedResourcesBtnClick}>
<Icon.Trash2 className="icon-img" />
<span>{t("resources.clear-unused-resources")}</span>
</div>
<div className="flex flex-row justify-end items-center">
<Button color="danger" onClick={handleDeleteUnusedResourcesBtnClick} startDecorator={<Icon.Trash2 className="w-4 h-auto" />}>
<span>{t("resources.clear")}</span>
</Button>
</div>
</div>
{loadingState.isLoading ? (
@ -189,9 +166,9 @@ const ResourcesDialog: React.FC<Props> = (props: Props) => {
resources.map((resource) => (
<div key={resource.id} className="resource-container">
<span className="field-text id-text">{resource.id}</span>
<Tooltip title={resource.filename}>
<span className="field-text name-text">{resource.filename}</span>
</Tooltip>
<span className="field-text name-text" onClick={() => handleRenameBtnClick(resource)}>
{resource.filename}
</span>
<div className="buttons-container">
<Dropdown
actionsClassName="!w-28"
@ -203,12 +180,6 @@ const ResourcesDialog: React.FC<Props> = (props: Props) => {
>
{t("resources.preview")}
</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
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)}

View file

@ -4,6 +4,7 @@ import { Link } from "react-router-dom";
import { useTranslation } from "react-i18next";
import { useLocationStore, useUserStore } from "../store/module";
import showDailyReviewDialog from "./DailyReviewDialog";
import showResourcesDialog from "./ResourcesDialog";
import showSettingDialog from "./SettingDialog";
import UserBanner from "./UserBanner";
import UsageHeatMap from "./UsageHeatMap";
@ -38,6 +39,9 @@ const Sidebar = () => {
<Link to="/explore" className="btn action-btn">
<span className="icon">🏂</span> {t("common.explore")}
</Link>
<button className="btn action-btn" onClick={() => showResourcesDialog()}>
<span className="icon">🗂</span> {t("sidebar.resources")}
</button>
{!userStore.isVisitorMode() && (
<>
<button className="btn action-btn" onClick={handleSettingBtnClick}>

View file

@ -5,7 +5,6 @@ import { getMemoStats } from "../helpers/api";
import * as utils from "../helpers/utils";
import Icon from "./Icon";
import Dropdown from "./common/Dropdown";
import showResourcesDialog from "./ResourcesDialog";
import showArchivedMemoDialog from "./ArchivedMemoDialog";
import showAboutSiteDialog from "./AboutSiteDialog";
import "../less/user-banner.less";
@ -51,10 +50,6 @@ const UserBanner = () => {
locationStore.clearQuery();
}, []);
const handleResourcesBtnClick = () => {
showResourcesDialog();
};
const handleArchivedBtnClick = () => {
showArchivedMemoDialog();
};
@ -82,17 +77,11 @@ const UserBanner = () => {
<>
{!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
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}
>
<span className="mr-1">🗂</span> {t("sidebar.archived")}
<span className="mr-1">🗃</span> {t("sidebar.archived")}
</button>
</>
)}

View file

@ -153,8 +153,12 @@ export function getResourceList() {
return axios.get<ResponseObject<Resource[]>>("/api/resource");
}
export function uploadFile(formData: FormData) {
return axios.post<ResponseObject<Resource>>("/api/resource", formData);
export function createResource(resourceCreate: ResourceCreate) {
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) {

View file

@ -9,7 +9,7 @@
@apply relative w-full h-auto mx-auto flex flex-row justify-start sm:justify-center items-start;
> .sidebar-wrapper {
@apply flex-shrink-0 ml-calc;
@apply flex-shrink-0 h-full ml-calc;
}
> .memos-wrapper {

View file

@ -62,11 +62,11 @@
@apply w-full truncate text-base pr-2 last:pr-0;
&.id-text {
@apply col-span-2;
@apply col-span-1;
}
&.name-text {
@apply col-span-4;
@apply col-span-5;
}
}
}

View file

@ -75,7 +75,7 @@
"warning-text": "Are you sure to delete this resource? THIS ACTION IS IRREVERSIABLE❗",
"linked-amount": "Linked memo amount",
"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❗",
"no-unused-resources": "No unused resources",
"name": "Name"

View file

@ -75,7 +75,7 @@
"warning-text": "确定删除这个资源么?此操作不可逆❗",
"linked-amount": "链接的 Memo 数量",
"rename": "重命名",
"clear-unused-resources": "清除无用资源",
"clear": "清除",
"warning-text-unused": "确定删除这些无用资源么?此操作不可逆❗",
"no-unused-resources": "无可删除的资源",
"name": "资源名称"

View file

@ -2,6 +2,8 @@ import store, { useAppSelector } from "../";
import { patchResource, setResources, deleteResource } from "../reducer/resource";
import * as api from "../../helpers/api";
const MAX_FILE_SIZE = 32 << 20;
const convertResponseModelResource = (resource: Resource): Resource => {
return {
...resource,
@ -24,16 +26,22 @@ export const useResourceStore = () => {
store.dispatch(setResources(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;
if (size > 64 << 20) {
return Promise.reject("overload max size: 8MB");
if (size > MAX_FILE_SIZE) {
return Promise.reject("overload max size: 32MB");
}
const formData = new FormData();
formData.append("file", file, filename);
const { data } = (await api.uploadFile(formData)).data;
const { data } = (await api.createResourceWithBlob(formData)).data;
const resource = convertResponseModelResource(data);
const resourceList = state.resources;
store.dispatch(setResources([resource, ...resourceList]));

View file

@ -7,6 +7,11 @@ const theme = extendTheme({
size: "sm",
},
},
JoyInput: {
defaultProps: {
size: "sm",
},
},
JoySelect: {
defaultProps: {
size: "sm",

View file

@ -7,12 +7,18 @@ interface Resource {
updatedTs: TimeStamp;
filename: string;
externalLink: string;
type: string;
size: string;
linkedMemoAmount: number;
}
interface ResourceCreate {
filename: string;
externalLink: string;
}
interface ResourcePatch {
id: ResourceId;
filename?: string;

View file

@ -1,3 +1,6 @@
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)}`;
};