Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Share tags #4198

Closed
wants to merge 12 commits into from
Prev Previous commit
Next Next commit
Merge main into branch
RoccoSmit committed Jan 9, 2025

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
commit 9c2ae0ea16e9dba4d3e89779875ce4885e0e9bad
421 changes: 247 additions & 174 deletions proto/gen/api/v1/memo_service.pb.go

Large diffs are not rendered by default.

11 changes: 9 additions & 2 deletions proto/gen/api/v1/workspace_setting_service.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions proto/gen/apidocs.swagger.yaml
Original file line number Diff line number Diff line change
@@ -511,6 +511,9 @@ paths:
$ref: '#/definitions/googlerpcStatus'
parameters:
- name: filter
description: |-
Filter is used to filter users returned in the list.
Format: "username == 'frank'"
description: |-
Filter is used to filter users returned in the list.
Format: "username == 'frank'"
@@ -1969,6 +1972,7 @@ definitions:
The name of the memo.
Format: memos/{id}
id is the system generated id.
readOnly: true
uid:
type: string
description: The user defined id of the memo.
2 changes: 2 additions & 0 deletions proto/gen/store/workspace_setting.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions server/router/api/v1/common.go
Original file line number Diff line number Diff line change
@@ -63,6 +63,10 @@ func unmarshalPageToken(s string, pageToken *v1pb.PageToken) error {
return nil
}

func isSuperUser(user *store.User) bool {
return user.Role == store.RoleAdmin || user.Role == store.RoleHost
}

func varPtr[T any](i T) *T {
return &i
}
284 changes: 0 additions & 284 deletions server/router/api/v1/memo_service.go
Original file line number Diff line number Diff line change
@@ -26,13 +26,6 @@ import (
"github.com/usememos/memos/store"
)

const (
// DefaultPageSize is the default page size for listing memos.
DefaultPageSize = 10
// DefaultTagPageSize is the default number of memos to loads tags for.
DefaultTagPageSize = 1_000_000
)

func (s *APIV1Service) CreateMemo(ctx context.Context, request *v1pb.CreateMemoRequest) (*v1pb.Memo, error) {
user, err := s.GetCurrentUser(ctx)
if err != nil {
@@ -638,283 +631,6 @@ func (s *APIV1Service) DeleteMemoTag(ctx context.Context, request *v1pb.DeleteMe
return &emptypb.Empty{}, nil
}

func (s *APIV1Service) convertMemoFromStore(ctx context.Context, memo *store.Memo, view v1pb.MemoView) (*v1pb.Memo, error) {
displayTs := memo.CreatedTs
workspaceMemoRelatedSetting, err := s.Store.GetWorkspaceMemoRelatedSetting(ctx)
if err != nil {
return nil, errors.Wrap(err, "failed to get workspace memo related setting")
}
if workspaceMemoRelatedSetting.DisplayWithUpdateTime {
displayTs = memo.UpdatedTs
}

name := fmt.Sprintf("%s%d", MemoNamePrefix, memo.ID)
memoMessage := &v1pb.Memo{
Name: name,
Uid: memo.UID,
RowStatus: convertRowStatusFromStore(memo.RowStatus),
Creator: fmt.Sprintf("%s%d", UserNamePrefix, memo.CreatorID),
CreateTime: timestamppb.New(time.Unix(memo.CreatedTs, 0)),
UpdateTime: timestamppb.New(time.Unix(memo.UpdatedTs, 0)),
DisplayTime: timestamppb.New(time.Unix(displayTs, 0)),
Content: memo.Content,
Visibility: convertVisibilityFromStore(memo.Visibility),
Pinned: memo.Pinned,
}
if memo.Payload != nil {
memoMessage.Property = convertMemoPropertyFromStore(memo.Payload.Property)
memoMessage.Location = convertLocationFromStore(memo.Payload.Location)
}
if memo.ParentID != nil {
parent := fmt.Sprintf("%s%d", MemoNamePrefix, *memo.ParentID)
memoMessage.Parent = &parent
}

// Fill content when view is MEMO_VIEW_FULL.
if view == v1pb.MemoView_MEMO_VIEW_FULL {
listMemoRelationsResponse, err := s.ListMemoRelations(ctx, &v1pb.ListMemoRelationsRequest{Name: name})
if err != nil {
return nil, errors.Wrap(err, "failed to list memo relations")
}
memoMessage.Relations = listMemoRelationsResponse.Relations

listMemoResourcesResponse, err := s.ListMemoResources(ctx, &v1pb.ListMemoResourcesRequest{Name: name})
if err != nil {
return nil, errors.Wrap(err, "failed to list memo resources")
}
memoMessage.Resources = listMemoResourcesResponse.Resources

listMemoReactionsResponse, err := s.ListMemoReactions(ctx, &v1pb.ListMemoReactionsRequest{Name: name})
if err != nil {
return nil, errors.Wrap(err, "failed to list memo reactions")
}
memoMessage.Reactions = listMemoReactionsResponse.Reactions

nodes, err := parser.Parse(tokenizer.Tokenize(memo.Content))
if err != nil {
return nil, errors.Wrap(err, "failed to parse content")
}
memoMessage.Nodes = convertFromASTNodes(nodes)

snippet, err := getMemoContentSnippet(memo.Content)
if err != nil {
return nil, errors.Wrap(err, "failed to get memo content snippet")
}
memoMessage.Snippet = snippet
}

return memoMessage, nil
}

func convertMemoPropertyFromStore(property *storepb.MemoPayload_Property) *v1pb.MemoProperty {
if property == nil {
return nil
}
return &v1pb.MemoProperty{
Tags: property.Tags,
HasLink: property.HasLink,
HasTaskList: property.HasTaskList,
HasCode: property.HasCode,
HasIncompleteTasks: property.HasIncompleteTasks,
}
}

func convertLocationFromStore(location *storepb.MemoPayload_Location) *v1pb.Location {
if location == nil {
return nil
}
return &v1pb.Location{
Placeholder: location.Placeholder,
Latitude: location.Latitude,
Longitude: location.Longitude,
}
}

func convertLocationToStore(location *v1pb.Location) *storepb.MemoPayload_Location {
if location == nil {
return nil
}
return &storepb.MemoPayload_Location{
Placeholder: location.Placeholder,
Latitude: location.Latitude,
Longitude: location.Longitude,
}
}

func convertVisibilityFromStore(visibility store.Visibility) v1pb.Visibility {
switch visibility {
case store.Private:
return v1pb.Visibility_PRIVATE
case store.Protected:
return v1pb.Visibility_PROTECTED
case store.Public:
return v1pb.Visibility_PUBLIC
default:
return v1pb.Visibility_VISIBILITY_UNSPECIFIED
}
}

func convertVisibilityToStore(visibility v1pb.Visibility) store.Visibility {
switch visibility {
case v1pb.Visibility_PRIVATE:
return store.Private
case v1pb.Visibility_PROTECTED:
return store.Protected
case v1pb.Visibility_PUBLIC:
return store.Public
default:
return store.Private
}
}

func (s *APIV1Service) buildMemoTagsFindWithFilter(ctx context.Context, find *store.FindMemo, request *v1pb.ListMemosRequest) error {
workspaceMemoRelatedSetting, err := s.Store.GetWorkspaceMemoRelatedSetting(ctx)
if err != nil {
return status.Errorf(codes.Internal, "failed to get workspace memo related setting")
}

if !workspaceMemoRelatedSetting.ShareTags {
return status.Errorf(codes.Internal, "Sharing tags is not enabled")
}

user, err := s.GetCurrentUser(ctx)
if err != nil {
return status.Errorf(codes.Internal, "failed to get current user")
}

if user == nil {
return status.Errorf(codes.Internal, "User need to be set to share tags")
}

find.CreatorID = &user.ID
find.ExcludeComments = true
find.ExcludeContent = true
find.ShareTags = true
find.RowStatus = varPtr(store.Normal)

request.PageSize = DefaultTagPageSize

return nil
}

func (s *APIV1Service) buildMemoFindWithFilter(ctx context.Context, find *store.FindMemo, filter string) error {
if find.PayloadFind == nil {
find.PayloadFind = &store.FindMemoPayload{}
}
if filter != "" {
filter, err := parseMemoFilter(filter)
if err != nil {
return status.Errorf(codes.InvalidArgument, "invalid filter: %v", err)
}
if len(filter.ContentSearch) > 0 {
find.ContentSearch = filter.ContentSearch
}
if len(filter.Visibilities) > 0 {
find.VisibilityList = filter.Visibilities
}
if filter.TagSearch != nil {
if find.PayloadFind == nil {
find.PayloadFind = &store.FindMemoPayload{}
}
find.PayloadFind.TagSearch = filter.TagSearch
}
if filter.OrderByPinned {
find.OrderByPinned = filter.OrderByPinned
}
if filter.OrderByTimeAsc {
find.OrderByTimeAsc = filter.OrderByTimeAsc
}
if filter.DisplayTimeAfter != nil {
workspaceMemoRelatedSetting, err := s.Store.GetWorkspaceMemoRelatedSetting(ctx)
if err != nil {
return status.Errorf(codes.Internal, "failed to get workspace memo related setting")
}
if workspaceMemoRelatedSetting.DisplayWithUpdateTime {
find.UpdatedTsAfter = filter.DisplayTimeAfter
} else {
find.CreatedTsAfter = filter.DisplayTimeAfter
}
}
if filter.DisplayTimeBefore != nil {
workspaceMemoRelatedSetting, err := s.Store.GetWorkspaceMemoRelatedSetting(ctx)
if err != nil {
return status.Errorf(codes.Internal, "failed to get workspace memo related setting")
}
if workspaceMemoRelatedSetting.DisplayWithUpdateTime {
find.UpdatedTsBefore = filter.DisplayTimeBefore
} else {
find.CreatedTsBefore = filter.DisplayTimeBefore
}
}
if filter.Creator != nil {
userID, err := ExtractUserIDFromName(*filter.Creator)
if err != nil {
return errors.Wrap(err, "invalid user name")
}
user, err := s.Store.GetUser(ctx, &store.FindUser{
ID: &userID,
})
if err != nil {
return status.Errorf(codes.Internal, "failed to get user")
}
if user == nil {
return status.Errorf(codes.NotFound, "user not found")
}
find.CreatorID = &user.ID
}
if filter.RowStatus != nil {
find.RowStatus = filter.RowStatus
}
if filter.Random {
find.Random = filter.Random
}
if filter.Limit != nil {
find.Limit = filter.Limit
}
if filter.IncludeComments {
find.ExcludeComments = false
}
if filter.HasLink {
find.PayloadFind.HasLink = true
}
if filter.HasTaskList {
find.PayloadFind.HasTaskList = true
}
if filter.HasCode {
find.PayloadFind.HasCode = true
}
if filter.HasIncompleteTasks {
find.PayloadFind.HasIncompleteTasks = true
}
}

user, err := s.GetCurrentUser(ctx)
if err != nil {
return status.Errorf(codes.Internal, "failed to get current user")
}
// If the user is not authenticated, only public memos are visible.
if user == nil {
if filter == "" {
// If no filter is provided, return an error.
return status.Errorf(codes.InvalidArgument, "filter is required for unauthenticated user")
}

find.VisibilityList = []store.Visibility{store.Public}
} else if find.CreatorID == nil || *find.CreatorID != user.ID {
// If creator is not specified or the creator is not the current user, only public and protected memos are visible.
find.VisibilityList = []store.Visibility{store.Public, store.Protected}
}

workspaceMemoRelatedSetting, err := s.Store.GetWorkspaceMemoRelatedSetting(ctx)
if err != nil {
return status.Errorf(codes.Internal, "failed to get workspace memo related setting")
}
if workspaceMemoRelatedSetting.DisplayWithUpdateTime {
find.OrderByUpdatedTs = true
}
return nil
}

func (s *APIV1Service) getContentLengthLimit(ctx context.Context) (int, error) {
workspaceMemoRelatedSetting, err := s.Store.GetWorkspaceMemoRelatedSetting(ctx)
if err != nil {
20 changes: 19 additions & 1 deletion web/src/store/v1/memoMetadata.ts
Original file line number Diff line number Diff line change
@@ -74,14 +74,32 @@ export const useMemoTagStore = create(
})),
);

export const useMemoTagStore = create(
combine(getDefaultState(), (set, get) => ({
setState: (state: State) => set(state),
getState: () => get(),
fetchMemoTags: async () => {
const { memos } = await memoServiceClient.listMemos({
view: MemoView.MEMO_VIEW_TAGS,
});
const memoMap = { ...get().dataMapByName };
for (const memo of memos) {
memoMap[memo.name] = memo;
}
set({ stateId: uniqueId(), dataMapByName: memoMap });
return { memos };
},
})),
);

export const useMemoTagList = () => {
const location = useLocation();
const workspaceSettingStore = useWorkspaceSettingStore();
const shareTags: boolean =
workspaceSettingStore.getWorkspaceSettingByKey(WorkspaceSettingKey.MEMO_RELATED).memoRelatedSetting?.shareTags || false;

const memoStore = location?.pathname !== Routes.EXPLORE && shareTags ? useMemoTagStore() : useMemoMetadataStore();
const data = Object.values(memoStore.getState().dataMapByName);
const memos = Object.values(memoStore.getState().dataMapByName);
const tagAmounts: Record<string, number> = {};
memos.forEach((memo) => {
const tagSet = new Set<string>();
You are viewing a condensed version of this merge commit. You can view the full changes here.