feat: enhance memo sorting functionality to support multiple fields

This commit is contained in:
Steven 2025-10-20 23:41:58 +08:00
parent 95de5cc700
commit b4ea7d843f
30 changed files with 113 additions and 55 deletions

View file

@ -291,7 +291,9 @@ message ListMemosRequest {
// Optional. The order to sort results by.
// Default to "display_time desc".
// Example: "display_time desc" or "create_time asc"
// Supports comma-separated list of fields following AIP-132.
// Example: "pinned desc, display_time desc" or "create_time asc"
// Supported fields: pinned, display_time, create_time, update_time, name
string order_by = 4 [(google.api.field_behavior) = OPTIONAL];
// Optional. Filter to apply to the list results.

View file

@ -1,6 +1,6 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.9
// protoc-gen-go v1.36.10
// protoc (unknown)
// source: api/v1/activity_service.proto

View file

@ -1,6 +1,6 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.9
// protoc-gen-go v1.36.10
// protoc (unknown)
// source: api/v1/attachment_service.proto

View file

@ -1,6 +1,6 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.9
// protoc-gen-go v1.36.10
// protoc (unknown)
// source: api/v1/auth_service.proto

View file

@ -1,6 +1,6 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.9
// protoc-gen-go v1.36.10
// protoc (unknown)
// source: api/v1/common.proto

View file

@ -1,6 +1,6 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.9
// protoc-gen-go v1.36.10
// protoc (unknown)
// source: api/v1/idp_service.proto

View file

@ -1,6 +1,6 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.9
// protoc-gen-go v1.36.10
// protoc (unknown)
// source: api/v1/inbox_service.proto

View file

@ -1,6 +1,6 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.9
// protoc-gen-go v1.36.10
// protoc (unknown)
// source: api/v1/markdown_service.proto

View file

@ -1,6 +1,6 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.9
// protoc-gen-go v1.36.10
// protoc (unknown)
// source: api/v1/memo_service.proto
@ -564,7 +564,9 @@ type ListMemosRequest struct {
State State `protobuf:"varint,3,opt,name=state,proto3,enum=memos.api.v1.State" json:"state,omitempty"`
// Optional. The order to sort results by.
// Default to "display_time desc".
// Example: "display_time desc" or "create_time asc"
// Supports comma-separated list of fields following AIP-132.
// Example: "pinned desc, display_time desc" or "create_time asc"
// Supported fields: pinned, display_time, create_time, update_time, name
OrderBy string `protobuf:"bytes,4,opt,name=order_by,json=orderBy,proto3" json:"order_by,omitempty"`
// Optional. Filter to apply to the list results.
// Filter is a CEL expression to filter memos.

View file

@ -1,6 +1,6 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.9
// protoc-gen-go v1.36.10
// protoc (unknown)
// source: api/v1/shortcut_service.proto

View file

@ -1,6 +1,6 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.9
// protoc-gen-go v1.36.10
// protoc (unknown)
// source: api/v1/user_service.proto

View file

@ -1,6 +1,6 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.9
// protoc-gen-go v1.36.10
// protoc (unknown)
// source: api/v1/workspace_service.proto

View file

@ -656,7 +656,9 @@ paths:
description: |-
Optional. The order to sort results by.
Default to "display_time desc".
Example: "display_time desc" or "create_time asc"
Supports comma-separated list of fields following AIP-132.
Example: "pinned desc, display_time desc" or "create_time asc"
Supported fields: pinned, display_time, create_time, update_time, name
schema:
type: string
- name: filter

View file

@ -1,6 +1,6 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.9
// protoc-gen-go v1.36.10
// protoc (unknown)
// source: store/activity.proto

View file

@ -1,6 +1,6 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.9
// protoc-gen-go v1.36.10
// protoc (unknown)
// source: store/attachment.proto

View file

@ -1,6 +1,6 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.9
// protoc-gen-go v1.36.10
// protoc (unknown)
// source: store/idp.proto

View file

@ -1,6 +1,6 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.9
// protoc-gen-go v1.36.10
// protoc (unknown)
// source: store/inbox.proto

View file

@ -1,6 +1,6 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.9
// protoc-gen-go v1.36.10
// protoc (unknown)
// source: store/memo.proto

View file

@ -1,6 +1,6 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.9
// protoc-gen-go v1.36.10
// protoc (unknown)
// source: store/user_setting.proto

View file

@ -1,6 +1,6 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.9
// protoc-gen-go v1.36.10
// protoc (unknown)
// source: store/workspace_setting.proto

View file

@ -875,30 +875,55 @@ func substring(s string, length int) string {
}
// parseMemoOrderBy parses the order_by field and sets the appropriate ordering in memoFind.
// Follows AIP-132: supports comma-separated list of fields with optional "desc" suffix.
// Example: "pinned desc, display_time desc" or "create_time asc".
func (*APIV1Service) parseMemoOrderBy(orderBy string, memoFind *store.FindMemo) error {
// Parse order_by field like "display_time desc" or "create_time asc"
parts := strings.Fields(strings.TrimSpace(orderBy))
if len(parts) == 0 {
if strings.TrimSpace(orderBy) == "" {
return errors.New("empty order_by")
}
field := parts[0]
direction := "desc" // default
if len(parts) > 1 {
direction = strings.ToLower(parts[1])
if direction != "asc" && direction != "desc" {
return errors.Errorf("invalid order direction: %s, must be 'asc' or 'desc'", parts[1])
// Split by comma to support multiple sort fields per AIP-132.
fields := strings.Split(orderBy, ",")
// Track if we've seen pinned field.
hasPinned := false
for _, field := range fields {
parts := strings.Fields(strings.TrimSpace(field))
if len(parts) == 0 {
continue
}
fieldName := parts[0]
fieldDirection := "desc" // default per AIP-132 (we use desc as default for time fields)
if len(parts) > 1 {
fieldDirection = strings.ToLower(parts[1])
if fieldDirection != "asc" && fieldDirection != "desc" {
return errors.Errorf("invalid order direction: %s, must be 'asc' or 'desc'", parts[1])
}
}
switch fieldName {
case "pinned":
hasPinned = true
memoFind.OrderByPinned = true
// Note: pinned is always DESC (true first) regardless of direction specified.
case "display_time", "create_time", "name":
// Only set if this is the first time field we encounter.
if !memoFind.OrderByUpdatedTs {
memoFind.OrderByTimeAsc = fieldDirection == "asc"
}
case "update_time":
memoFind.OrderByUpdatedTs = true
memoFind.OrderByTimeAsc = fieldDirection == "asc"
default:
return errors.Errorf("unsupported order field: %s, supported fields are: pinned, display_time, create_time, update_time, name", fieldName)
}
}
switch field {
case "display_time", "create_time", "name":
memoFind.OrderByTimeAsc = direction == "asc"
case "update_time":
memoFind.OrderByUpdatedTs = true
memoFind.OrderByTimeAsc = direction == "asc"
default:
return errors.Errorf("unsupported order field: %s, supported fields are: display_time, create_time, update_time, name", field)
// If only pinned was specified, still need to set a default time ordering.
if hasPinned && !memoFind.OrderByUpdatedTs && len(fields) == 1 {
memoFind.OrderByTimeAsc = false // default to desc
}
return nil

View file

@ -106,6 +106,9 @@ func (d *DB) ListMemos(ctx context.Context, find *store.FindMemo) ([]*store.Memo
order = "ASC"
}
orderBy := []string{}
if find.OrderByPinned {
orderBy = append(orderBy, "`pinned` DESC")
}
if find.OrderByUpdatedTs {
orderBy = append(orderBy, "`updated_ts` "+order)
} else {

View file

@ -97,6 +97,9 @@ func (d *DB) ListMemos(ctx context.Context, find *store.FindMemo) ([]*store.Memo
order = "ASC"
}
orderBy := []string{}
if find.OrderByPinned {
orderBy = append(orderBy, "pinned DESC")
}
if find.OrderByUpdatedTs {
orderBy = append(orderBy, "updated_ts "+order)
} else {

View file

@ -98,6 +98,9 @@ func (d *DB) ListMemos(ctx context.Context, find *store.FindMemo) ([]*store.Memo
order = "ASC"
}
orderBy := []string{}
if find.OrderByPinned {
orderBy = append(orderBy, "`pinned` DESC")
}
if find.OrderByUpdatedTs {
orderBy = append(orderBy, "`updated_ts` "+order)
} else {

View file

@ -76,6 +76,7 @@ type FindMemo struct {
Offset *int
// Ordering
OrderByPinned bool
OrderByUpdatedTs bool
OrderByTimeAsc bool
}

View file

@ -64,7 +64,7 @@ const (
// Before 0.22, migration history had inconsistent versioning that needed normalization.
migrationHistoryNormalizedVersion = "0.22"
// Mode constants for profile mode
// Mode constants for profile mode.
modeProd = "prod"
modeDemo = "demo"
)

View file

@ -34,14 +34,19 @@ const Archived = observer(() => {
listSort={(memos: Memo[]) =>
memos
.filter((memo) => memo.state === State.ARCHIVED)
.sort((a, b) =>
viewStore.state.orderByTimeAsc
.sort((a, b) => {
// First, sort by pinned status (pinned memos first)
if (a.pinned !== b.pinned) {
return b.pinned ? 1 : -1;
}
// Then sort by display time
return viewStore.state.orderByTimeAsc
? dayjs(a.displayTime).unix() - dayjs(b.displayTime).unix()
: dayjs(b.displayTime).unix() - dayjs(a.displayTime).unix(),
)
: dayjs(b.displayTime).unix() - dayjs(a.displayTime).unix();
})
}
state={State.ARCHIVED}
orderBy={viewStore.state.orderByTimeAsc ? "display_time asc" : "display_time desc"}
orderBy={viewStore.state.orderByTimeAsc ? "pinned desc, display_time asc" : "pinned desc, display_time desc"}
filter={memoFitler}
/>
);

View file

@ -63,13 +63,18 @@ const Home = observer(() => {
listSort={(memos: Memo[]) =>
memos
.filter((memo) => memo.state === State.NORMAL)
.sort((a, b) =>
viewStore.state.orderByTimeAsc
.sort((a, b) => {
// First, sort by pinned status (pinned memos first)
if (a.pinned !== b.pinned) {
return b.pinned ? 1 : -1;
}
// Then sort by display time
return viewStore.state.orderByTimeAsc
? dayjs(a.displayTime).unix() - dayjs(b.displayTime).unix()
: dayjs(b.displayTime).unix() - dayjs(a.displayTime).unix(),
)
: dayjs(b.displayTime).unix() - dayjs(a.displayTime).unix();
})
}
orderBy={viewStore.state.orderByTimeAsc ? "display_time asc" : "display_time desc"}
orderBy={viewStore.state.orderByTimeAsc ? "pinned desc, display_time asc" : "pinned desc, display_time desc"}
filter={memoFilter}
/>
</div>

View file

@ -96,13 +96,18 @@ const UserProfile = observer(() => {
listSort={(memos: Memo[]) =>
memos
.filter((memo) => memo.state === State.NORMAL)
.sort((a, b) =>
viewStore.state.orderByTimeAsc
.sort((a, b) => {
// First, sort by pinned status (pinned memos first)
if (a.pinned !== b.pinned) {
return b.pinned ? 1 : -1;
}
// Then sort by display time
return viewStore.state.orderByTimeAsc
? dayjs(a.displayTime).unix() - dayjs(b.displayTime).unix()
: dayjs(b.displayTime).unix() - dayjs(a.displayTime).unix(),
)
: dayjs(b.displayTime).unix() - dayjs(a.displayTime).unix();
})
}
orderBy={viewStore.state.orderByTimeAsc ? "display_time asc" : "display_time desc"}
orderBy={viewStore.state.orderByTimeAsc ? "pinned desc, display_time asc" : "pinned desc, display_time desc"}
filter={memoFilter}
/>
</>

View file

@ -195,7 +195,9 @@ export interface ListMemosRequest {
/**
* Optional. The order to sort results by.
* Default to "display_time desc".
* Example: "display_time desc" or "create_time asc"
* Supports comma-separated list of fields following AIP-132.
* Example: "pinned desc, display_time desc" or "create_time asc"
* Supported fields: pinned, display_time, create_time, update_time, name
*/
orderBy: string;
/**