mirror of
https://github.com/usememos/memos.git
synced 2024-11-24 14:42:58 +03:00
feat: request pagination for resource(#1425)
* feat: add support for resource page on frontend * [WIP]feat: add backend support for limit and offset search * feat: add reducer to add resource * support fetch all resource when first search * beautify the fetch ui * restore file * feat: add all resource before clear resource * eslint * i18n * chore:change the nane * chore: change the name of param * eslint * feat: setIsComplete to true when first loading resource fully * fix the bug of fetch * feat change finally to then * feat: add await and async to clear and search * feat: return all resource when fetch * chore: change variable name * Update web/src/pages/ResourcesDashboard.tsx Co-authored-by: boojack <stevenlgtm@gmail.com> * fix missing const value --------- Co-authored-by: boojack <stevenlgtm@gmail.com>
This commit is contained in:
parent
fab3dac70a
commit
424f10e180
@ -45,6 +45,10 @@ type ResourceFind struct {
|
||||
Filename *string `json:"filename"`
|
||||
MemoID *int
|
||||
GetBlob bool
|
||||
|
||||
// Pagination
|
||||
Limit *int
|
||||
Offset *int
|
||||
}
|
||||
|
||||
type ResourcePatch struct {
|
||||
|
@ -256,6 +256,13 @@ func (s *Server) registerResourceRoutes(g *echo.Group) {
|
||||
resourceFind := &api.ResourceFind{
|
||||
CreatorID: &userID,
|
||||
}
|
||||
if limit, err := strconv.Atoi(c.QueryParam("limit")); err == nil {
|
||||
resourceFind.Limit = &limit
|
||||
}
|
||||
if offset, err := strconv.Atoi(c.QueryParam("offset")); err == nil {
|
||||
resourceFind.Offset = &offset
|
||||
}
|
||||
|
||||
list, err := s.Store.FindResourceList(ctx, resourceFind)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch resource list").SetInternal(err)
|
||||
|
@ -312,6 +312,13 @@ func (s *Store) findResourceListImpl(ctx context.Context, tx *sql.Tx, find *api.
|
||||
GROUP BY resource.id
|
||||
ORDER BY resource.id DESC
|
||||
`, strings.Join(fields, ", "), strings.Join(where, " AND "))
|
||||
if find.Limit != nil {
|
||||
query = fmt.Sprintf("%s LIMIT %d", query, *find.Limit)
|
||||
if find.Offset != nil {
|
||||
query = fmt.Sprintf("%s OFFSET %d", query, *find.Offset)
|
||||
}
|
||||
}
|
||||
|
||||
rows, err := tx.QueryContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, FormatError(err)
|
||||
|
@ -164,6 +164,17 @@ export function getResourceList() {
|
||||
return axios.get<ResponseObject<Resource[]>>("/api/resource");
|
||||
}
|
||||
|
||||
export function getResourceListWithLimit(resourceFind?: ResourceFind) {
|
||||
const queryList = [];
|
||||
if (resourceFind?.offset) {
|
||||
queryList.push(`offset=${resourceFind.offset}`);
|
||||
}
|
||||
if (resourceFind?.limit) {
|
||||
queryList.push(`limit=${resourceFind.limit}`);
|
||||
}
|
||||
return axios.get<ResponseObject<Resource[]>>(`/api/resource?${queryList.join("&")}`);
|
||||
}
|
||||
|
||||
export function createResource(resourceCreate: ResourceCreate) {
|
||||
return axios.post<ResponseObject<Resource>>("/api/resource", resourceCreate);
|
||||
}
|
||||
|
@ -245,6 +245,8 @@
|
||||
"message": {
|
||||
"no-memos": "no memos 🌃",
|
||||
"memos-ready": "all memos are ready 🎉",
|
||||
"no-resource": "no resource 🌃",
|
||||
"resource-ready": "all resource are ready 🎉",
|
||||
"restored-successfully": "Restored successfully",
|
||||
"memo-updated-datetime": "Memo created datetime changed.",
|
||||
"invalid-created-datetime": "Invalid created datetime.",
|
||||
|
@ -218,6 +218,8 @@
|
||||
"message": {
|
||||
"no-memos": "沒有 Memo 了 🌃",
|
||||
"memos-ready": "所有 Memo 已就緒 🎉",
|
||||
"no-resource": "沒有 Resource 了 🌃",
|
||||
"memos-resource": "所有 Resource 已就緒 🎉",
|
||||
"restored-successfully": "還原成功",
|
||||
"memo-updated-datetime": "Memo 建立日期時間已更改。",
|
||||
"invalid-created-datetime": "建立的日期時間無效。",
|
||||
|
@ -221,6 +221,8 @@
|
||||
"message": {
|
||||
"no-memos": "没有 Memo 了 🌃",
|
||||
"memos-ready": "所有 Memo 已就绪 🎉",
|
||||
"no-resource": "没有 Resource 了 🌃",
|
||||
"resource-ready": "所有 Resource 已就绪 🎉",
|
||||
"restored-successfully": "恢复成功",
|
||||
"memo-updated-datetime": "Memo 创建日期时间已更改。",
|
||||
"invalid-created-datetime": "创建的日期时间无效。",
|
||||
|
@ -16,6 +16,7 @@ import { showCommonDialog } from "@/components/Dialog/CommonDialog";
|
||||
import showChangeResourceFilenameDialog from "@/components/ChangeResourceFilenameDialog";
|
||||
import showPreviewImageDialog from "@/components/PreviewImageDialog";
|
||||
import showCreateResourceDialog from "@/components/CreateResourceDialog";
|
||||
import { DEFAULT_MEMO_LIMIT } from "@/helpers/consts";
|
||||
|
||||
const ResourcesDashboard = () => {
|
||||
const { t } = useTranslation();
|
||||
@ -26,16 +27,20 @@ const ResourcesDashboard = () => {
|
||||
const [listStyle, setListStyle] = useState<"GRID" | "TABLE">("GRID");
|
||||
const [queryText, setQueryText] = useState<string>("");
|
||||
const [dragActive, setDragActive] = useState(false);
|
||||
const [isComplete, setIsComplete] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
resourceStore
|
||||
.fetchResourceList()
|
||||
.fetchResourceListWithLimit(DEFAULT_MEMO_LIMIT)
|
||||
.then((fetchedResource) => {
|
||||
if (fetchedResource.length < DEFAULT_MEMO_LIMIT) {
|
||||
setIsComplete(true);
|
||||
}
|
||||
loadingState.setFinish();
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
toast.error(error.response.data.message);
|
||||
})
|
||||
.finally(() => {
|
||||
loadingState.setFinish();
|
||||
});
|
||||
}, []);
|
||||
|
||||
@ -47,29 +52,31 @@ const ResourcesDashboard = () => {
|
||||
setSelectedList(selectedList.filter((resId) => resId !== resourceId));
|
||||
};
|
||||
|
||||
const handleDeleteUnusedResourcesBtnClick = () => {
|
||||
const handleDeleteUnusedResourcesBtnClick = async () => {
|
||||
let warningText = t("resources.warning-text-unused");
|
||||
const unusedResources = resources.filter((resource) => {
|
||||
if (resource.linkedMemoAmount === 0) {
|
||||
warningText = warningText + `\n- ${resource.filename}`;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
if (unusedResources.length === 0) {
|
||||
toast.success(t("resources.no-unused-resources"));
|
||||
return;
|
||||
}
|
||||
showCommonDialog({
|
||||
title: t("resources.delete-resource"),
|
||||
content: warningText,
|
||||
style: "warning",
|
||||
dialogName: "delete-unused-resources",
|
||||
onConfirm: async () => {
|
||||
for (const resource of unusedResources) {
|
||||
await resourceStore.deleteResourceById(resource.id);
|
||||
await loadAllResources((allResources: Resource[]) => {
|
||||
const unusedResources = allResources.filter((resource) => {
|
||||
if (resource.linkedMemoAmount === 0) {
|
||||
warningText = warningText + `\n- ${resource.filename}`;
|
||||
return true;
|
||||
}
|
||||
},
|
||||
return false;
|
||||
});
|
||||
if (unusedResources.length === 0) {
|
||||
toast.success(t("resources.no-unused-resources"));
|
||||
return;
|
||||
}
|
||||
showCommonDialog({
|
||||
title: t("resources.delete-resource"),
|
||||
content: warningText,
|
||||
style: "warning",
|
||||
dialogName: "delete-unused-resources",
|
||||
onConfirm: async () => {
|
||||
for (const resource of unusedResources) {
|
||||
await resourceStore.deleteResourceById(resource.id);
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
@ -136,9 +143,44 @@ const ResourcesDashboard = () => {
|
||||
toast.success(t("message.succeed-copy-resource-link"));
|
||||
};
|
||||
|
||||
const handleSearchResourceInputChange = (query: string) => {
|
||||
setQueryText(query);
|
||||
setSelectedList([]);
|
||||
const handleFetchMoreResourceBtnClick = async () => {
|
||||
try {
|
||||
const fetchedResource = await resourceStore.fetchResourceListWithLimit(DEFAULT_MEMO_LIMIT, resources.length);
|
||||
if (fetchedResource.length < DEFAULT_MEMO_LIMIT) {
|
||||
setIsComplete(true);
|
||||
} else {
|
||||
setIsComplete(false);
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
toast.error(error.response.data.message);
|
||||
}
|
||||
};
|
||||
|
||||
const loadAllResources = async (resolve: (allResources: Resource[]) => void) => {
|
||||
if (!isComplete) {
|
||||
loadingState.setLoading();
|
||||
try {
|
||||
const allResources = await resourceStore.fetchResourceList();
|
||||
loadingState.setFinish();
|
||||
setIsComplete(true);
|
||||
resolve(allResources);
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
toast.error(error.response.data.message);
|
||||
}
|
||||
} else {
|
||||
resolve(resources);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSearchResourceInputChange = async (query: string) => {
|
||||
// to prevent first tiger when page is loaded
|
||||
if (query === queryText) return;
|
||||
await loadAllResources(() => {
|
||||
setQueryText(query);
|
||||
setSelectedList([]);
|
||||
});
|
||||
};
|
||||
|
||||
const resourceList = useMemo(
|
||||
@ -304,6 +346,23 @@ const ResourcesDashboard = () => {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col justify-start items-center w-full my-6">
|
||||
<p className="text-sm text-gray-400 italic">
|
||||
{isComplete ? (
|
||||
resources.length === 0 ? (
|
||||
t("message.no-resource")
|
||||
) : (
|
||||
t("message.resource-ready")
|
||||
)
|
||||
) : (
|
||||
<>
|
||||
<span className="cursor-pointer hover:text-green-600" onClick={handleFetchMoreResourceBtnClick}>
|
||||
{t("memo-list.fetch-more")}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
@ -1,6 +1,7 @@
|
||||
import store, { useAppSelector } from "../";
|
||||
import { patchResource, setResources, deleteResource } from "../reducer/resource";
|
||||
import { patchResource, setResources, deleteResource, upsertResources } from "../reducer/resource";
|
||||
import * as api from "../../helpers/api";
|
||||
import { DEFAULT_MEMO_LIMIT } from "../../helpers/consts";
|
||||
|
||||
const MAX_FILE_SIZE = 32 << 20;
|
||||
|
||||
@ -26,6 +27,16 @@ export const useResourceStore = () => {
|
||||
store.dispatch(setResources(resourceList));
|
||||
return resourceList;
|
||||
},
|
||||
async fetchResourceListWithLimit(limit = DEFAULT_MEMO_LIMIT, offset?: number): Promise<Resource[]> {
|
||||
const resourceFind: ResourceFind = {
|
||||
limit,
|
||||
offset,
|
||||
};
|
||||
const { data } = (await api.getResourceListWithLimit(resourceFind)).data;
|
||||
const resourceList = data.map((m) => convertResponseModelResource(m));
|
||||
store.dispatch(upsertResources(resourceList));
|
||||
return resourceList;
|
||||
},
|
||||
async createResource(resourceCreate: ResourceCreate): Promise<Resource> {
|
||||
const { data } = (await api.createResource(resourceCreate)).data;
|
||||
const resource = convertResponseModelResource(data);
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
||||
import { uniqBy } from "lodash-es";
|
||||
|
||||
interface State {
|
||||
resources: Resource[];
|
||||
@ -16,6 +17,12 @@ const resourceSlice = createSlice({
|
||||
resources: action.payload,
|
||||
};
|
||||
},
|
||||
upsertResources: (state, action: PayloadAction<Resource[]>) => {
|
||||
return {
|
||||
...state,
|
||||
resources: uniqBy([...state.resources, ...action.payload], "id"),
|
||||
};
|
||||
},
|
||||
patchResource: (state, action: PayloadAction<Partial<Resource>>) => {
|
||||
return {
|
||||
...state,
|
||||
@ -42,6 +49,6 @@ const resourceSlice = createSlice({
|
||||
},
|
||||
});
|
||||
|
||||
export const { setResources, patchResource, deleteResource } = resourceSlice.actions;
|
||||
export const { setResources, upsertResources, patchResource, deleteResource } = resourceSlice.actions;
|
||||
|
||||
export default resourceSlice.reducer;
|
||||
|
5
web/src/types/modules/resource.d.ts
vendored
5
web/src/types/modules/resource.d.ts
vendored
@ -24,3 +24,8 @@ interface ResourcePatch {
|
||||
id: ResourceId;
|
||||
filename?: string;
|
||||
}
|
||||
|
||||
interface ResourceFind {
|
||||
offset?: number;
|
||||
limit?: number;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user