diff --git a/.dockerignore b/.dockerignore index b8fe7484..a0ae8ea5 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,2 +1 @@ web/node_modules -web/yarn.lock diff --git a/api/resource.go b/api/resource.go index d08ca072..0184c9b2 100644 --- a/api/resource.go +++ b/api/resource.go @@ -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 { diff --git a/server/resource.go b/server/resource.go index 60b7df7a..fb7d4ab8 100644 --- a/server/resource.go +++ b/server/resource.go @@ -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) } diff --git a/store/resource.go b/store/resource.go index eb8703b9..1e469fb1 100644 --- a/store/resource.go +++ b/store/resource.go @@ -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, diff --git a/web/src/components/CreateTagDialog.tsx b/web/src/components/CreateTagDialog.tsx index ec025076..04ddd4dd 100644 --- a/web/src/components/CreateTagDialog.tsx +++ b/web/src/components/CreateTagDialog.tsx @@ -90,6 +90,7 @@ const CreateTagDialog: React.FC = (props: Props) => {
= (props: Props) => { }; return ( -
+
e.stopPropagation()}> {children}
diff --git a/web/src/components/MemoEditor.tsx b/web/src/components/MemoEditor.tsx index d3736d20..41856d71 100644 --- a/web/src/components/MemoEditor.tsx +++ b/web/src/components/MemoEditor.tsx @@ -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({ isUploadingResource: false, fullscreen: false, + isRequesting: false, }); const [allowSave, setAllowSave] = useState(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 = () => {
-
-
-
handleUploadFileBtnClick()}> - - {t("resources.upload")} -
+
+
+
-
-
- - {t("resources.clear-unused-resources")} -
+
+
{loadingState.isLoading ? ( @@ -189,9 +166,9 @@ const ResourcesDialog: React.FC = (props: Props) => { resources.map((resource) => (
{resource.id} - - {resource.filename} - + handleRenameBtnClick(resource)}> + {resource.filename} +
= (props: Props) => { > {t("resources.preview")} - {!userStore.isVisitorMode() && ( <> )} diff --git a/web/src/helpers/api.ts b/web/src/helpers/api.ts index 010c03c0..94c79e78 100644 --- a/web/src/helpers/api.ts +++ b/web/src/helpers/api.ts @@ -153,8 +153,12 @@ export function getResourceList() { return axios.get>("/api/resource"); } -export function uploadFile(formData: FormData) { - return axios.post>("/api/resource", formData); +export function createResource(resourceCreate: ResourceCreate) { + return axios.post>("/api/resource", resourceCreate); +} + +export function createResourceWithBlob(formData: FormData) { + return axios.post>("/api/resource/blob", formData); } export function deleteResourceById(id: ResourceId) { diff --git a/web/src/less/home.less b/web/src/less/home.less index e51fbd92..47d97c00 100644 --- a/web/src/less/home.less +++ b/web/src/less/home.less @@ -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 { diff --git a/web/src/less/resources-dialog.less b/web/src/less/resources-dialog.less index 6b4c636e..a71e6738 100644 --- a/web/src/less/resources-dialog.less +++ b/web/src/less/resources-dialog.less @@ -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; } } } diff --git a/web/src/locales/en.json b/web/src/locales/en.json index 1b7fbd7e..36b73127 100644 --- a/web/src/locales/en.json +++ b/web/src/locales/en.json @@ -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" diff --git a/web/src/locales/zh.json b/web/src/locales/zh.json index 546dcb4a..42da16d2 100644 --- a/web/src/locales/zh.json +++ b/web/src/locales/zh.json @@ -75,7 +75,7 @@ "warning-text": "确定删除这个资源么?此操作不可逆❗", "linked-amount": "链接的 Memo 数量", "rename": "重命名", - "clear-unused-resources": "清除无用资源", + "clear": "清除", "warning-text-unused": "确定删除这些无用资源么?此操作不可逆❗", "no-unused-resources": "无可删除的资源", "name": "资源名称" diff --git a/web/src/store/module/resource.ts b/web/src/store/module/resource.ts index 8d87253a..f9307dbd 100644 --- a/web/src/store/module/resource.ts +++ b/web/src/store/module/resource.ts @@ -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 { + async createResource(resourceCreate: ResourceCreate): Promise { + 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 { 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])); diff --git a/web/src/theme/index.ts b/web/src/theme/index.ts index 2dae5fd9..a9405043 100644 --- a/web/src/theme/index.ts +++ b/web/src/theme/index.ts @@ -7,6 +7,11 @@ const theme = extendTheme({ size: "sm", }, }, + JoyInput: { + defaultProps: { + size: "sm", + }, + }, JoySelect: { defaultProps: { size: "sm", diff --git a/web/src/types/modules/resource.d.ts b/web/src/types/modules/resource.d.ts index d6245cf1..2436bc76 100644 --- a/web/src/types/modules/resource.d.ts +++ b/web/src/types/modules/resource.d.ts @@ -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; diff --git a/web/src/utils/resource.ts b/web/src/utils/resource.ts index c52795de..cfe54d5f 100644 --- a/web/src/utils/resource.ts +++ b/web/src/utils/resource.ts @@ -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)}`; };