feat: customize memo list sorting rules (#312)

* chore: update .gitignore

* feat: 添加Memo列表按更新时间排序

* fix go-static-checks

* update

* update

* update Memo.tsx/MemoList.tsx

* handle conflict

Co-authored-by: boojack <stevenlgtm@gmail.com>
This commit is contained in:
Zeng1998
2022-10-19 21:00:34 +08:00
committed by GitHub
parent 24154c95f2
commit bf5b7e747d
14 changed files with 109 additions and 19 deletions

3
.gitignore vendored
View File

@ -11,3 +11,6 @@ web/dist
build build
.DS_Store .DS_Store
# Jetbrains
.idea

View File

@ -16,6 +16,8 @@ const (
UserSettingEditorFontStyleKey UserSettingKey = "editorFontStyle" UserSettingEditorFontStyleKey UserSettingKey = "editorFontStyle"
// UserSettingEditorFontStyleKey is the key type for mobile editor style. // UserSettingEditorFontStyleKey is the key type for mobile editor style.
UserSettingMobileEditorStyleKey UserSettingKey = "mobileEditorStyle" UserSettingMobileEditorStyleKey UserSettingKey = "mobileEditorStyle"
// UserSettingMemoSortOptionKey is the key type for sort time option.
UserSettingMemoSortOptionKey UserSettingKey = "memoSortOption"
) )
// String returns the string format of UserSettingKey type. // String returns the string format of UserSettingKey type.
@ -29,6 +31,8 @@ func (key UserSettingKey) String() string {
return "editorFontFamily" return "editorFontFamily"
case UserSettingMobileEditorStyleKey: case UserSettingMobileEditorStyleKey:
return "mobileEditorStyle" return "mobileEditorStyle"
case UserSettingMemoSortOptionKey:
return "memoSortOption"
} }
return "" return ""
} }
@ -38,6 +42,7 @@ var (
UserSettingMemoVisibilityValue = []Visibility{Privite, Protected, Public} UserSettingMemoVisibilityValue = []Visibility{Privite, Protected, Public}
UserSettingEditorFontStyleValue = []string{"normal", "mono"} UserSettingEditorFontStyleValue = []string{"normal", "mono"}
UserSettingMobileEditorStyleValue = []string{"normal", "float"} UserSettingMobileEditorStyleValue = []string{"normal", "float"}
UserSettingSortTimeOptionKeyValue = []string{"created_ts", "updated_ts"}
) )
type UserSetting struct { type UserSetting struct {
@ -122,6 +127,23 @@ func (upsert UserSettingUpsert) Validate() error {
if invalid { if invalid {
return fmt.Errorf("invalid user setting mobile editor style value") return fmt.Errorf("invalid user setting mobile editor style value")
} }
} else if upsert.Key == UserSettingMemoSortOptionKey {
memoSortOption := "created_ts"
err := json.Unmarshal([]byte(upsert.Value), &memoSortOption)
if err != nil {
return fmt.Errorf("failed to unmarshal user setting memo sort option")
}
invalid := true
for _, value := range UserSettingSortTimeOptionKeyValue {
if memoSortOption == value {
invalid = false
break
}
}
if invalid {
return fmt.Errorf("invalid user setting memo sort option value")
}
} else { } else {
return fmt.Errorf("invalid user setting key") return fmt.Errorf("invalid user setting key")
} }

View File

@ -7,6 +7,7 @@ import { useNavigate } from "react-router-dom";
import "dayjs/locale/zh"; import "dayjs/locale/zh";
import { UNKNOWN_ID } from "../helpers/consts"; import { UNKNOWN_ID } from "../helpers/consts";
import { editorStateService, locationService, memoService, userService } from "../services"; import { editorStateService, locationService, memoService, userService } from "../services";
import { useAppSelector } from "../store";
import Icon from "./Icon"; import Icon from "./Icon";
import toastHelper from "./Toast"; import toastHelper from "./Toast";
import MemoContent from "./MemoContent"; import MemoContent from "./MemoContent";
@ -22,19 +23,21 @@ interface Props {
memo: Memo; memo: Memo;
} }
export const getFormatedMemoCreatedAtStr = (createdTs: number, locale = "en"): string => { export const getFormatedMemoTimeStr = (time: number, locale = "en"): string => {
if (Date.now() - createdTs < 1000 * 60 * 60 * 24) { if (Date.now() - time < 1000 * 60 * 60 * 24) {
return dayjs(createdTs).locale(locale).fromNow(); return dayjs(time).locale(locale).fromNow();
} else { } else {
return dayjs(createdTs).locale(locale).format("YYYY/MM/DD HH:mm:ss"); return dayjs(time).locale(locale).format("YYYY/MM/DD HH:mm:ss");
} }
}; };
const Memo: React.FC<Props> = (props: Props) => { const Memo: React.FC<Props> = (props: Props) => {
const memo = props.memo; const memo = props.memo;
const user = useAppSelector((state) => state.user.user);
const { t, i18n } = useTranslation(); const { t, i18n } = useTranslation();
const navigate = useNavigate(); const navigate = useNavigate();
const [createdAtStr, setCreatedAtStr] = useState<string>(getFormatedMemoCreatedAtStr(memo.createdTs, i18n.language)); const [createdAtStr, setCreatedAtStr] = useState<string>(getFormatedMemoTimeStr(memo.createdTs, i18n.language));
const [updatedAtStr, setUpdatedAtStr] = useState<string>(getFormatedMemoTimeStr(memo.updatedTs, i18n.language));
const memoContainerRef = useRef<HTMLDivElement>(null); const memoContainerRef = useRef<HTMLDivElement>(null);
const isVisitorMode = userService.isVisitorMode(); const isVisitorMode = userService.isVisitorMode();
@ -42,7 +45,8 @@ const Memo: React.FC<Props> = (props: Props) => {
let intervalFlag: any = -1; let intervalFlag: any = -1;
if (Date.now() - memo.createdTs < 1000 * 60 * 60 * 24) { if (Date.now() - memo.createdTs < 1000 * 60 * 60 * 24) {
intervalFlag = setInterval(() => { intervalFlag = setInterval(() => {
setCreatedAtStr(getFormatedMemoCreatedAtStr(memo.createdTs, i18n.language)); setCreatedAtStr(getFormatedMemoTimeStr(memo.createdTs, i18n.language));
setUpdatedAtStr(getFormatedMemoTimeStr(memo.updatedTs, i18n.language));
}, 1000 * 1); }, 1000 * 1);
} }
@ -182,12 +186,13 @@ const Memo: React.FC<Props> = (props: Props) => {
editorStateService.setEditMemoWithId(memo.id); editorStateService.setEditMemoWithId(memo.id);
}; };
const timeStr = user?.setting.memoSortOption === "created_ts" ? createdAtStr : `${t("common.update-on")} ${updatedAtStr}`;
return ( return (
<div className={`memo-wrapper ${"memos-" + memo.id} ${memo.pinned ? "pinned" : ""}`} ref={memoContainerRef}> <div className={`memo-wrapper ${"memos-" + memo.id} ${memo.pinned ? "pinned" : ""}`} ref={memoContainerRef}>
<div className="memo-top-wrapper"> <div className="memo-top-wrapper">
<div className="status-text-container"> <div className="status-text-container">
<span className="time-text" onClick={handleShowMemoStoryDialog}> <span className="time-text" onClick={handleShowMemoStoryDialog}>
{createdAtStr} {timeStr}
</span> </span>
{memo.visibility !== "PRIVATE" && !isVisitorMode && ( {memo.visibility !== "PRIVATE" && !isVisitorMode && (
<span className={`status-text ${memo.visibility.toLocaleLowerCase()}`}>{memo.visibility}</span> <span className={`status-text ${memo.visibility.toLocaleLowerCase()}`}>{memo.visibility}</span>

View File

@ -174,6 +174,7 @@ const MemoEditor: React.FC = () => {
}); });
locationService.clearQuery(); locationService.clearQuery();
} }
locationService.setUpdatedFlag();
} catch (error: any) { } catch (error: any) {
console.error(error); console.error(error);
toastHelper.error(error.response.data.message); toastHelper.error(error.response.data.message);

View File

@ -12,6 +12,8 @@ import "../less/memo-list.less";
const MemoList = () => { const MemoList = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const query = useAppSelector((state) => state.location.query); const query = useAppSelector((state) => state.location.query);
const updatedTime = useAppSelector((state) => state.location.updatedTime);
const user = useAppSelector((state) => state.user.user);
const { memos, isFetching } = useAppSelector((state) => state.memo); const { memos, isFetching } = useAppSelector((state) => state.memo);
const { tag: tagQuery, duration, type: memoType, text: textQuery, shortcutId } = query ?? {}; const { tag: tagQuery, duration, type: memoType, text: textQuery, shortcutId } = query ?? {};
@ -70,6 +72,11 @@ const MemoList = () => {
const pinnedMemos = shownMemos.filter((m) => m.pinned); const pinnedMemos = shownMemos.filter((m) => m.pinned);
const unpinnedMemos = shownMemos.filter((m) => !m.pinned); const unpinnedMemos = shownMemos.filter((m) => !m.pinned);
const memoSorting = (m1: Memo, m2: Memo) => {
return user?.setting.memoSortOption === "created_ts" ? m2.createdTs - m1.createdTs : m2.updatedTs - m1.updatedTs;
};
pinnedMemos.sort(memoSorting);
unpinnedMemos.sort(memoSorting);
const sortedMemos = pinnedMemos.concat(unpinnedMemos).filter((m) => m.rowStatus === "NORMAL"); const sortedMemos = pinnedMemos.concat(unpinnedMemos).filter((m) => m.rowStatus === "NORMAL");
useEffect(() => { useEffect(() => {
@ -82,14 +89,14 @@ const MemoList = () => {
console.error(error); console.error(error);
toastHelper.error(error.response.data.message); toastHelper.error(error.response.data.message);
}); });
}, []); }, [updatedTime]);
useEffect(() => { useEffect(() => {
const pageWrapper = document.body.querySelector(".page-wrapper"); const pageWrapper = document.body.querySelector(".page-wrapper");
if (pageWrapper) { if (pageWrapper) {
pageWrapper.scrollTo(0, 0); pageWrapper.scrollTo(0, 0);
} }
}, [query]); }, [query, updatedTime]);
return ( return (
<div className="memo-list-container"> <div className="memo-list-container">

View File

@ -1,7 +1,7 @@
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { globalService, userService } from "../../services"; import { globalService, userService } from "../../services";
import { useAppSelector } from "../../store"; import { useAppSelector } from "../../store";
import { VISIBILITY_SELECTOR_ITEMS } from "../../helpers/consts"; import { VISIBILITY_SELECTOR_ITEMS, MEMO_SORT_OPTION_SELECTOR_ITEMS } from "../../helpers/consts";
import Selector from "../common/Selector"; import Selector from "../common/Selector";
import "../../less/settings/preferences-section.less"; import "../../less/settings/preferences-section.less";
@ -52,6 +52,13 @@ const PreferencesSection = () => {
}; };
}); });
const memoSortOptionSelectorItems = MEMO_SORT_OPTION_SELECTOR_ITEMS.map((item) => {
return {
value: item.value,
text: t(`setting.preference-section.${item.value}`),
};
});
const handleLocaleChanged = async (value: string) => { const handleLocaleChanged = async (value: string) => {
await userService.upsertUserSetting("locale", value); await userService.upsertUserSetting("locale", value);
globalService.setLocale(value as Locale); globalService.setLocale(value as Locale);
@ -69,6 +76,10 @@ const PreferencesSection = () => {
await userService.upsertUserSetting("mobileEditorStyle", value); await userService.upsertUserSetting("mobileEditorStyle", value);
}; };
const handleMemoSortOptionChanged = async (value: string) => {
await userService.upsertUserSetting("memoSortOption", value);
};
return ( return (
<div className="section-container preferences-section-container"> <div className="section-container preferences-section-container">
<p className="title-text">{t("common.basic")}</p> <p className="title-text">{t("common.basic")}</p>
@ -104,6 +115,15 @@ const PreferencesSection = () => {
handleValueChanged={handleMobileEditorStyleChanged} handleValueChanged={handleMobileEditorStyleChanged}
/> />
</label> </label>
<label className="form-label selector">
<span className="normal-text">{t("setting.preference-section.default-memo-sort-option")}</span>
<Selector
className="ml-2 w-32"
value={setting.memoSortOption}
dataSource={memoSortOptionSelectorItems}
handleValueChanged={handleMemoSortOptionChanged}
/>
</label>
</div> </div>
); );
}; };

View File

@ -13,4 +13,9 @@ export const VISIBILITY_SELECTOR_ITEMS = [
{ text: "PRIVATE", value: "PRIVATE" }, { text: "PRIVATE", value: "PRIVATE" },
]; ];
export const MEMO_SORT_OPTION_SELECTOR_ITEMS = [
{ text: "created_ts", value: "created_ts" },
{ text: "created_ts", value: "updated_ts" },
];
export const TAB_SPACE_WIDTH = 2; export const TAB_SPACE_WIDTH = 2;

View File

@ -37,7 +37,8 @@
"tags": "Tags", "tags": "Tags",
"yourself": "Yourself", "yourself": "Yourself",
"archived-at": "Archived at", "archived-at": "Archived at",
"changed": "changed" "changed": "changed",
"update-on": "Update on"
}, },
"slogan": "An open source, self-hosted knowledge base that works with a SQLite db file.", "slogan": "An open source, self-hosted knowledge base that works with a SQLite db file.",
"auth": { "auth": {
@ -128,7 +129,10 @@
"preference-section": { "preference-section": {
"default-memo-visibility": "Default memo visibility", "default-memo-visibility": "Default memo visibility",
"editor-font-style": "Editor font style", "editor-font-style": "Editor font style",
"mobile-editor-style": "Mobile editor style" "mobile-editor-style": "Mobile editor style",
"default-memo-sort-option": "Sort by created time/updated time",
"created_ts": "Created Time",
"updated_ts": "Updated Time"
}, },
"member-section": { "member-section": {
"create-a-member": "Create a member" "create-a-member": "Create a member"

View File

@ -37,7 +37,8 @@
"tags": "Thẻ", "tags": "Thẻ",
"yourself": "Chính bạn", "yourself": "Chính bạn",
"archived-at": "Lưu trữ lúc", "archived-at": "Lưu trữ lúc",
"changed": "đã thay đổi" "changed": "đã thay đổi",
"update-on": "Cập nhật"
}, },
"slogan": "Một mã nguồn mở, tự bạn lưu lại mọi thứ bạn biết dựa trên SQLite db.", "slogan": "Một mã nguồn mở, tự bạn lưu lại mọi thứ bạn biết dựa trên SQLite db.",
"auth": { "auth": {
@ -128,7 +129,10 @@
"preference-section": { "preference-section": {
"default-memo-visibility": "Chế độ memo mặc định", "default-memo-visibility": "Chế độ memo mặc định",
"editor-font-style": "Thay đổi font cho trình soạn thảo", "editor-font-style": "Thay đổi font cho trình soạn thảo",
"mobile-editor-style": "Vị trí editor trên mobile" "mobile-editor-style": "Vị trí editor trên mobile",
"default-memo-sort-option": "Sắp xếp theo thời gian đã tạo",
"created_ts": "tạo thời gian",
"updated_ts": "Thời gian cập nhật"
}, },
"member-section": { "member-section": {
"create-a-member": "Tạo thành viên" "create-a-member": "Tạo thành viên"

View File

@ -37,7 +37,8 @@
"tags": "全部标签", "tags": "全部标签",
"yourself": "你自己", "yourself": "你自己",
"archived-at": "归档于", "archived-at": "归档于",
"changed": "已更改" "changed": "已更改",
"update-on": "更新于"
}, },
"slogan": "一个开源的、支持私有化部署的碎片化知识卡片管理工具。", "slogan": "一个开源的、支持私有化部署的碎片化知识卡片管理工具。",
"auth": { "auth": {
@ -128,7 +129,10 @@
"preference-section": { "preference-section": {
"default-memo-visibility": "默认 Memo 可见性", "default-memo-visibility": "默认 Memo 可见性",
"editor-font-style": "编辑器字体样式", "editor-font-style": "编辑器字体样式",
"mobile-editor-style": "Mobile editor style" "mobile-editor-style": "Mobile editor style",
"default-memo-sort-option": "按创建时间/更新时间排序",
"created_ts": "创建时间",
"updated_ts": "更新时间"
}, },
"member-section": { "member-section": {
"create-a-member": "创建成员" "create-a-member": "创建成员"

View File

@ -1,6 +1,7 @@
import { stringify } from "qs"; import { stringify } from "qs";
import store from "../store"; import store from "../store";
import { setQuery, setPathname, Query, updateStateWithLocation } from "../store/modules/location"; import { setQuery, setPathname, setUpdatedTime, Query, updateStateWithLocation } from "../store/modules/location";
import { getTimeStampByDate } from "../helpers/utils";
const updateLocationUrl = (method: "replace" | "push" = "replace") => { const updateLocationUrl = (method: "replace" | "push" = "replace") => {
const { query, pathname, hash } = store.getState().location; const { query, pathname, hash } = store.getState().location;
@ -112,6 +113,10 @@ const locationService = {
); );
updateLocationUrl(); updateLocationUrl();
}, },
setUpdatedFlag: () => {
store.dispatch(setUpdatedTime(getTimeStampByDate(new Date()).toString()));
},
}; };
export default locationService; export default locationService;

View File

@ -8,6 +8,7 @@ const defauleSetting: Setting = {
memoVisibility: "PRIVATE", memoVisibility: "PRIVATE",
editorFontStyle: "normal", editorFontStyle: "normal",
mobileEditorStyle: "normal", mobileEditorStyle: "normal",
memoSortOption: "created_ts",
}; };
export const convertResponseModelUser = (user: User): User => { export const convertResponseModelUser = (user: User): User => {

View File

@ -17,6 +17,7 @@ interface State {
pathname: string; pathname: string;
hash: string; hash: string;
query: Query; query: Query;
updatedTime: string;
} }
const getValidPathname = (pathname: string): string => { const getValidPathname = (pathname: string): string => {
@ -35,6 +36,7 @@ const getStateFromLocation = () => {
pathname: getValidPathname(pathname), pathname: getValidPathname(pathname),
hash: hash, hash: hash,
query: {}, query: {},
updatedTime: "",
}; };
if (search !== "") { if (search !== "") {
@ -86,9 +88,15 @@ const locationSlice = createSlice({
}, },
}; };
}, },
setUpdatedTime: (state, action: PayloadAction<string>) => {
return {
...state,
updatedTime: action.payload,
};
},
}, },
}); });
export const { setPathname, setQuery, updateStateWithLocation } = locationSlice.actions; export const { setPathname, setQuery, setUpdatedTime, updateStateWithLocation } = locationSlice.actions;
export default locationSlice.reducer; export default locationSlice.reducer;

View File

@ -3,6 +3,7 @@ interface Setting {
memoVisibility: Visibility; memoVisibility: Visibility;
editorFontStyle: "normal" | "mono"; editorFontStyle: "normal" | "mono";
mobileEditorStyle: "normal" | "float"; mobileEditorStyle: "normal" | "float";
memoSortOption: "created_ts" | "updated_ts";
} }
interface UserLocaleSetting { interface UserLocaleSetting {