Commit 5334fdf1 authored by Steven's avatar Steven

chore: use api v2 in archived page

parent abc14217
......@@ -2,13 +2,19 @@ package v2
import (
"context"
"encoding/json"
"time"
"github.com/google/cel-go/cel"
"github.com/pkg/errors"
expr "google.golang.org/genproto/googleapis/api/expr/v1alpha1"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/timestamppb"
apiv1 "github.com/usememos/memos/api/v1"
"github.com/usememos/memos/plugin/gomark/parser"
"github.com/usememos/memos/plugin/gomark/parser/tokenizer"
apiv2pb "github.com/usememos/memos/proto/gen/api/v2"
"github.com/usememos/memos/store"
)
......@@ -32,7 +38,7 @@ func (s *APIV2Service) CreateMemo(ctx context.Context, request *apiv2pb.CreateMe
return nil, err
}
memoMessage, err := convertMemoFromStore(memo)
memoMessage, err := s.convertMemoFromStore(ctx, memo)
if err != nil {
return nil, errors.Wrap(err, "failed to convert memo")
}
......@@ -58,26 +64,34 @@ func (s *APIV2Service) ListMemos(ctx context.Context, request *apiv2pb.ListMemos
if filter.CreatedTsAfter != nil {
memoFind.CreatedTsAfter = filter.CreatedTsAfter
}
if filter.Creator != nil {
username, err := ExtractUsernameFromName(*filter.Creator)
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "invalid creator name")
}
user, err := s.Store.GetUser(ctx, &store.FindUser{
Username: &username,
})
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get user")
}
if user == nil {
return nil, status.Errorf(codes.NotFound, "user not found")
}
memoFind.CreatorID = &user.ID
}
if filter.RowStatus != nil {
memoFind.RowStatus = filter.RowStatus
}
}
user, _ := getCurrentUser(ctx, s.Store)
// If the user is not authenticated, only public memos are visible.
if user == nil {
memoFind.VisibilityList = []store.Visibility{store.Public}
}
if request.CreatorId != nil {
memoFind.CreatorID = request.CreatorId
}
// Remove the private memos from the list if the user is not the creator.
if user != nil && request.CreatorId != nil && *request.CreatorId != user.ID {
var filteredVisibility []store.Visibility
for _, v := range memoFind.VisibilityList {
if v != store.Private {
filteredVisibility = append(filteredVisibility, v)
}
}
memoFind.VisibilityList = filteredVisibility
if user != nil && memoFind.CreatorID != nil && *memoFind.CreatorID != user.ID {
memoFind.VisibilityList = []store.Visibility{store.Public, store.Protected}
}
if request.PageSize != 0 {
......@@ -93,7 +107,7 @@ func (s *APIV2Service) ListMemos(ctx context.Context, request *apiv2pb.ListMemos
memoMessages := make([]*apiv2pb.Memo, len(memos))
for i, memo := range memos {
memoMessage, err := convertMemoFromStore(memo)
memoMessage, err := s.convertMemoFromStore(ctx, memo)
if err != nil {
return nil, errors.Wrap(err, "failed to convert memo")
}
......@@ -129,7 +143,7 @@ func (s *APIV2Service) GetMemo(ctx context.Context, request *apiv2pb.GetMemoRequ
}
}
memoMessage, err := convertMemoFromStore(memo)
memoMessage, err := s.convertMemoFromStore(ctx, memo)
if err != nil {
return nil, errors.Wrap(err, "failed to convert memo")
}
......@@ -182,7 +196,7 @@ func (s *APIV2Service) ListMemoComments(ctx context.Context, request *apiv2pb.Li
return nil, status.Errorf(codes.Internal, "failed to get memo")
}
if memo != nil {
memoMessage, err := convertMemoFromStore(memo)
memoMessage, err := s.convertMemoFromStore(ctx, memo)
if err != nil {
return nil, errors.Wrap(err, "failed to convert memo")
}
......@@ -196,17 +210,75 @@ func (s *APIV2Service) ListMemoComments(ctx context.Context, request *apiv2pb.Li
return response, nil
}
func (s *APIV2Service) convertMemoFromStore(ctx context.Context, memo *store.Memo) (*apiv2pb.Memo, error) {
rawNodes, err := parser.Parse(tokenizer.Tokenize(memo.Content))
if err != nil {
return nil, errors.Wrap(err, "failed to parse memo content")
}
displayTs := memo.CreatedTs
if displayWithUpdatedTs, err := s.getMemoDisplayWithUpdatedTsSettingValue(ctx); err == nil && displayWithUpdatedTs {
displayTs = memo.UpdatedTs
}
return &apiv2pb.Memo{
Id: int32(memo.ID),
RowStatus: convertRowStatusFromStore(memo.RowStatus),
CreateTime: timestamppb.New(time.Unix(memo.CreatedTs, 0)),
UpdateTime: timestamppb.New(time.Unix(memo.UpdatedTs, 0)),
DisplayTime: timestamppb.New(time.Unix(displayTs, 0)),
CreatorId: int32(memo.CreatorID),
Content: memo.Content,
Nodes: convertFromASTNodes(rawNodes),
Visibility: convertVisibilityFromStore(memo.Visibility),
Pinned: memo.Pinned,
}, nil
}
func (s *APIV2Service) getMemoDisplayWithUpdatedTsSettingValue(ctx context.Context) (bool, error) {
memoDisplayWithUpdatedTsSetting, err := s.Store.GetSystemSetting(ctx, &store.FindSystemSetting{
Name: apiv1.SystemSettingMemoDisplayWithUpdatedTsName.String(),
})
if err != nil {
return false, errors.Wrap(err, "failed to find system setting")
}
memoDisplayWithUpdatedTs := false
if memoDisplayWithUpdatedTsSetting != nil {
err = json.Unmarshal([]byte(memoDisplayWithUpdatedTsSetting.Value), &memoDisplayWithUpdatedTs)
if err != nil {
return false, errors.Wrap(err, "failed to unmarshal system setting value")
}
}
return memoDisplayWithUpdatedTs, nil
}
func convertVisibilityFromStore(visibility store.Visibility) apiv2pb.Visibility {
switch visibility {
case store.Private:
return apiv2pb.Visibility_PRIVATE
case store.Protected:
return apiv2pb.Visibility_PROTECTED
case store.Public:
return apiv2pb.Visibility_PUBLIC
default:
return apiv2pb.Visibility_VISIBILITY_UNSPECIFIED
}
}
// ListMemosFilterCELAttributes are the CEL attributes for ListMemosFilter.
var ListMemosFilterCELAttributes = []cel.EnvOption{
cel.Variable("visibility", cel.StringType),
cel.Variable("created_ts_before", cel.IntType),
cel.Variable("created_ts_after", cel.IntType),
cel.Variable("creator", cel.StringType),
cel.Variable("row_status", cel.StringType),
}
type ListMemosFilter struct {
Visibility *store.Visibility
CreatedTsBefore *int64
CreatedTsAfter *int64
Creator *string
RowStatus *store.RowStatus
}
func parseListMemosFilter(expression string) (*ListMemosFilter, error) {
......@@ -244,6 +316,14 @@ func findField(callExpr *expr.Expr_Call, filter *ListMemosFilter) {
createdTsAfter := callExpr.Args[1].GetConstExpr().GetInt64Value()
filter.CreatedTsAfter = &createdTsAfter
}
if idExpr.Name == "creator" {
creator := callExpr.Args[1].GetConstExpr().GetStringValue()
filter.Creator = &creator
}
if idExpr.Name == "row_status" {
rowStatus := store.RowStatus(callExpr.Args[1].GetConstExpr().GetStringValue())
filter.RowStatus = &rowStatus
}
return
}
}
......@@ -254,29 +334,3 @@ func findField(callExpr *expr.Expr_Call, filter *ListMemosFilter) {
}
}
}
func convertMemoFromStore(memo *store.Memo) (*apiv2pb.Memo, error) {
return &apiv2pb.Memo{
Id: int32(memo.ID),
RowStatus: convertRowStatusFromStore(memo.RowStatus),
CreatedTs: memo.CreatedTs,
UpdatedTs: memo.UpdatedTs,
CreatorId: int32(memo.CreatorID),
Content: memo.Content,
Visibility: convertVisibilityFromStore(memo.Visibility),
Pinned: memo.Pinned,
}, nil
}
func convertVisibilityFromStore(visibility store.Visibility) apiv2pb.Visibility {
switch visibility {
case store.Private:
return apiv2pb.Visibility_PRIVATE
case store.Protected:
return apiv2pb.Visibility_PROTECTED
case store.Public:
return apiv2pb.Visibility_PUBLIC
default:
return apiv2pb.Visibility_VISIBILITY_UNSPECIFIED
}
}
......@@ -95,7 +95,7 @@ func (s *APIV2Service) convertResourceFromStore(ctx context.Context, resource *s
return &apiv2pb.Resource{
Id: resource.ID,
CreatedTs: timestamppb.New(time.Unix(resource.CreatedTs, 0)),
CreateTime: timestamppb.New(time.Unix(resource.CreatedTs, 0)),
Filename: resource.Filename,
ExternalLink: resource.ExternalLink,
Type: resource.Type,
......
......@@ -6,6 +6,7 @@ import "api/v2/common.proto";
import "api/v2/markdown_service.proto";
import "google/api/annotations.proto";
import "google/api/client.proto";
import "google/protobuf/timestamp.proto";
option go_package = "gen/api/v2";
......@@ -51,17 +52,19 @@ message Memo {
int32 creator_id = 3;
int64 created_ts = 4;
google.protobuf.Timestamp create_time = 4;
int64 updated_ts = 5;
google.protobuf.Timestamp update_time = 5;
string content = 6;
google.protobuf.Timestamp display_time = 6;
repeated Node nodes = 7;
string content = 7;
Visibility visibility = 8;
repeated Node nodes = 8;
bool pinned = 9;
Visibility visibility = 9;
bool pinned = 10;
}
message CreateMemoRequest {
......@@ -81,8 +84,6 @@ message ListMemosRequest {
// Filter is used to filter memos returned in the list.
string filter = 3;
optional int32 creator_id = 4;
}
message ListMemosResponse {
......
......@@ -31,7 +31,7 @@ service ResourceService {
message Resource {
int32 id = 1;
google.protobuf.Timestamp created_ts = 2;
google.protobuf.Timestamp create_time = 2;
string filename = 3;
string external_link = 4;
string type = 5;
......
......@@ -1451,7 +1451,6 @@
| page | [int32](#int32) | | |
| page_size | [int32](#int32) | | |
| filter | [string](#string) | | Filter is used to filter memos returned in the list. |
| creator_id | [int32](#int32) | optional | |
......@@ -1484,8 +1483,9 @@
| id | [int32](#int32) | | |
| row_status | [RowStatus](#memos-api-v2-RowStatus) | | |
| creator_id | [int32](#int32) | | |
| created_ts | [int64](#int64) | | |
| updated_ts | [int64](#int64) | | |
| create_time | [google.protobuf.Timestamp](#google-protobuf-Timestamp) | | |
| update_time | [google.protobuf.Timestamp](#google-protobuf-Timestamp) | | |
| display_time | [google.protobuf.Timestamp](#google-protobuf-Timestamp) | | |
| content | [string](#string) | | |
| nodes | [Node](#memos-api-v2-Node) | repeated | |
| visibility | [Visibility](#memos-api-v2-Visibility) | | |
......@@ -1632,7 +1632,7 @@
| Field | Type | Label | Description |
| ----- | ---- | ----- | ----------- |
| id | [int32](#int32) | | |
| created_ts | [google.protobuf.Timestamp](#google-protobuf-Timestamp) | | |
| create_time | [google.protobuf.Timestamp](#google-protobuf-Timestamp) | | |
| filename | [string](#string) | | |
| external_link | [string](#string) | | |
| type | [string](#string) | | |
......
This diff is collapsed.
This diff is collapsed.
......@@ -2,11 +2,11 @@ import { Tooltip } from "@mui/joy";
import { toast } from "react-hot-toast";
import { getDateTimeString } from "@/helpers/datetime";
import { useMemoStore } from "@/store/module";
import { Memo } from "@/types/proto/api/v2/memo_service";
import { useTranslate } from "@/utils/i18n";
import { showCommonDialog } from "./Dialog/CommonDialog";
import Icon from "./Icon";
import MemoContentV1 from "./MemoContentV1";
import MemoResourceListView from "./MemoResourceListView";
import "@/less/memo.less";
interface Props {
......@@ -48,7 +48,7 @@ const ArchivedMemo: React.FC<Props> = (props: Props) => {
<div className={`memo-wrapper archived ${"memos-" + memo.id}`}>
<div className="memo-top-wrapper">
<div className="w-full max-w-[calc(100%-20px)] flex flex-row justify-start items-center mr-1">
<span className="text-sm text-gray-400 select-none">{getDateTimeString(memo.displayTs)}</span>
<span className="text-sm text-gray-400 select-none">{getDateTimeString(memo.displayTime)}</span>
</div>
<div className="flex flex-row justify-end items-center gap-x-2">
<Tooltip title={t("common.restore")} placement="top">
......@@ -63,8 +63,7 @@ const ArchivedMemo: React.FC<Props> = (props: Props) => {
</Tooltip>
</div>
</div>
<MemoContentV1 content={memo.content} />
<MemoResourceListView resourceList={memo.resourceList} />
<MemoContentV1 content={memo.content} nodes={memo.nodes} />
</div>
);
};
......
import { useEffect, useState } from "react";
import { toast } from "react-hot-toast";
import useLoading from "@/hooks/useLoading";
import { useMemoStore } from "@/store/module";
import { useTranslate } from "@/utils/i18n";
import ArchivedMemo from "./ArchivedMemo";
import { generateDialog } from "./Dialog";
import Icon from "./Icon";
import "@/less/archived-memo-dialog.less";
type Props = DialogProps;
const ArchivedMemoDialog: React.FC<Props> = (props: Props) => {
const t = useTranslate();
const { destroy } = props;
const memoStore = useMemoStore();
const memos = memoStore.state.memos;
const loadingState = useLoading();
const [archivedMemos, setArchivedMemos] = useState<Memo[]>([]);
useEffect(() => {
memoStore
.fetchArchivedMemos()
.then((result) => {
setArchivedMemos(result);
})
.catch((error) => {
console.error(error);
toast.error(error.response.data.message);
})
.finally(() => {
loadingState.setFinish();
});
}, [memos]);
return (
<>
<div className="dialog-header-container">
<p className="title-text">{t("memo.archived-memos")}</p>
<button className="btn close-btn" onClick={destroy}>
<Icon.X className="icon-img" />
</button>
</div>
<div className="dialog-content-container">
{loadingState.isLoading ? (
<div className="tip-text-container">
<p className="tip-text">{t("memo.fetching-data")}</p>
</div>
) : archivedMemos.length === 0 ? (
<div className="tip-text-container">
<p className="tip-text">{t("memo.no-archived-memos")}</p>
</div>
) : (
<div className="archived-memos-container">
{archivedMemos.map((memo) => (
<ArchivedMemo key={`${memo.id}-${memo.updatedTs}`} memo={memo} />
))}
</div>
)}
</div>
</>
);
};
export default function showArchivedMemoDialog(): void {
generateDialog(
{
className: "archived-memo-dialog",
dialogName: "archived-memo-dialog",
},
ArchivedMemoDialog,
{}
);
}
import { isUndefined } from "lodash-es";
import { useEffect, useRef, useState } from "react";
import { markdownServiceClient } from "@/grpcweb";
import { Node } from "@/types/proto/api/v2/markdown_service";
......@@ -5,16 +6,21 @@ import Renderer from "./Renderer";
interface Props {
content: string;
nodes?: Node[];
className?: string;
onMemoContentClick?: (e: React.MouseEvent) => void;
}
const MemoContentV1: React.FC<Props> = (props: Props) => {
const { className, content, onMemoContentClick } = props;
const [nodes, setNodes] = useState<Node[]>([]);
const [nodes, setNodes] = useState<Node[]>(props.nodes ?? []);
const memoContentContainerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!isUndefined(props.nodes)) {
return;
}
markdownServiceClient
.parseMarkdown({
markdown: content,
......@@ -22,7 +28,7 @@ const MemoContentV1: React.FC<Props> = (props: Props) => {
.then(({ nodes }) => {
setNodes(nodes);
});
}, [content]);
}, [content, props.nodes]);
const handleMemoContentClick = async (e: React.MouseEvent) => {
if (onMemoContentClick) {
......
......@@ -3,22 +3,23 @@ import toast from "react-hot-toast";
import ArchivedMemo from "@/components/ArchivedMemo";
import Empty from "@/components/Empty";
import MobileHeader from "@/components/MobileHeader";
import { memoServiceClient } from "@/grpcweb";
import useLoading from "@/hooks/useLoading";
import { useMemoStore } from "@/store/module";
import { Memo } from "@/types/proto/api/v2/memo_service";
import { useTranslate } from "@/utils/i18n";
const Archived = () => {
const t = useTranslate();
const memoStore = useMemoStore();
const loadingState = useLoading();
const [archivedMemos, setArchivedMemos] = useState<Memo[]>([]);
const memos = memoStore.state.memos;
useEffect(() => {
memoStore
.fetchArchivedMemos()
.then((result) => {
setArchivedMemos(result);
memoServiceClient
.listMemos({
filter: "row_status == 'ARCHIVED'",
})
.then(({ memos }) => {
setArchivedMemos(memos);
})
.catch((error) => {
console.error(error);
......@@ -27,7 +28,7 @@ const Archived = () => {
.finally(() => {
loadingState.setFinish();
});
}, [memos]);
}, []);
return (
<section className="@container w-full max-w-5xl min-h-full flex flex-col justify-start items-start sm:pt-3 md:pt-6 pb-8">
......@@ -45,7 +46,7 @@ const Archived = () => {
) : (
<div className="w-full flex flex-col justify-start items-start">
{archivedMemos.map((memo) => (
<ArchivedMemo key={`${memo.id}-${memo.updatedTs}`} memo={memo} />
<ArchivedMemo key={`${memo.id}-${memo.updateTime}`} memo={memo} />
))}
</div>
)}
......
......@@ -16,7 +16,7 @@ import { useTranslate } from "@/utils/i18n";
function groupResourcesByDate(resources: Resource[]) {
const grouped = new Map<number, Resource[]>();
resources.forEach((item) => {
const date = new Date(item.createdTs as any);
const date = new Date(item.createTime as any);
const year = date.getFullYear();
const month = date.getMonth() + 1;
const timestamp = Date.UTC(year, month - 1, 1);
......
export * from "./user";
export * from "./memo";
export * from "./memoCache";
export * from "./inbox";
export * from "./resourceName";
import { create } from "zustand";
import { combine } from "zustand/middleware";
import * as api from "@/helpers/api";
import { convertResponseModelMemo } from "../module";
import { memoServiceClient } from "@/grpcweb";
import { Memo } from "@/types/proto/api/v2/memo_service";
export const useMemoCacheStore = create(
combine({ memoById: new Map<MemoId, Memo>() }, (set, get) => ({
export const useMemoV1Store = create(
combine({ memoById: new Map<number, Memo>() }, (set, get) => ({
getState: () => get(),
getOrFetchMemoById: async (memoId: MemoId) => {
const memo = get().memoById.get(memoId);
getOrFetchMemoById: async (id: MemoId) => {
const memo = get().memoById.get(id);
if (memo) {
return memo;
}
const { data } = await api.getMemoById(memoId);
const formatedMemo = convertResponseModelMemo(data);
set((state) => {
state.memoById.set(memoId, formatedMemo);
return state;
const res = await memoServiceClient.getMemo({
id,
});
if (!res.memo) {
throw new Error("Memo not found");
}
return formatedMemo;
},
getMemoById: (memoId: MemoId) => {
return get().memoById.get(memoId);
},
setMemoCache: (memo: Memo) => {
set((state) => {
state.memoById.set(memo.id, memo);
state.memoById.set(id, res.memo as Memo);
return state;
});
return res.memo;
},
deleteMemoCache: (memoId: MemoId) => {
set((state) => {
state.memoById.delete(memoId);
return state;
});
getMemoById: (id: number) => {
return get().memoById.get(id);
},
}))
);
import { create } from "zustand";
import { combine } from "zustand/middleware";
import * as api from "@/helpers/api";
import { convertResponseModelMemo } from "../module";
export const useMemoCacheStore = create(
combine({ memoById: new Map<MemoId, Memo>() }, (set, get) => ({
getState: () => get(),
getOrFetchMemoById: async (memoId: MemoId) => {
const memo = get().memoById.get(memoId);
if (memo) {
return memo;
}
const { data } = await api.getMemoById(memoId);
const formatedMemo = convertResponseModelMemo(data);
set((state) => {
state.memoById.set(memoId, formatedMemo);
return state;
});
return formatedMemo;
},
getMemoById: (memoId: MemoId) => {
return get().memoById.get(memoId);
},
setMemoCache: (memo: Memo) => {
set((state) => {
state.memoById.set(memo.id, memo);
return state;
});
},
deleteMemoCache: (memoId: MemoId) => {
set((state) => {
state.memoById.delete(memoId);
return state;
});
},
}))
);
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment