diff --git a/.gitignore b/.gitignore index 48954faa..02cc87f4 100644 --- a/.gitignore +++ b/.gitignore @@ -10,4 +10,7 @@ web/dist # build folder build -.DS_Store \ No newline at end of file +.DS_Store + +# Jetbrains +.idea \ No newline at end of file diff --git a/api/user_setting.go b/api/user_setting.go index 20cf3bf6..c1858491 100644 --- a/api/user_setting.go +++ b/api/user_setting.go @@ -16,6 +16,8 @@ const ( UserSettingEditorFontStyleKey UserSettingKey = "editorFontStyle" // UserSettingEditorFontStyleKey is the key type for mobile editor style. UserSettingMobileEditorStyleKey UserSettingKey = "mobileEditorStyle" + // UserSettingMemoSortOptionKey is the key type for sort time option. + UserSettingMemoSortOptionKey UserSettingKey = "memoSortOption" ) // String returns the string format of UserSettingKey type. @@ -29,6 +31,8 @@ func (key UserSettingKey) String() string { return "editorFontFamily" case UserSettingMobileEditorStyleKey: return "mobileEditorStyle" + case UserSettingMemoSortOptionKey: + return "memoSortOption" } return "" } @@ -38,6 +42,7 @@ var ( UserSettingMemoVisibilityValue = []Visibility{Privite, Protected, Public} UserSettingEditorFontStyleValue = []string{"normal", "mono"} UserSettingMobileEditorStyleValue = []string{"normal", "float"} + UserSettingSortTimeOptionKeyValue = []string{"created_ts", "updated_ts"} ) type UserSetting struct { @@ -122,6 +127,23 @@ func (upsert UserSettingUpsert) Validate() error { if invalid { 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 { return fmt.Errorf("invalid user setting key") } diff --git a/web/src/components/Memo.tsx b/web/src/components/Memo.tsx index 8a8d6a9f..16659fe9 100644 --- a/web/src/components/Memo.tsx +++ b/web/src/components/Memo.tsx @@ -7,6 +7,7 @@ import { useNavigate } from "react-router-dom"; import "dayjs/locale/zh"; import { UNKNOWN_ID } from "../helpers/consts"; import { editorStateService, locationService, memoService, userService } from "../services"; +import { useAppSelector } from "../store"; import Icon from "./Icon"; import toastHelper from "./Toast"; import MemoContent from "./MemoContent"; @@ -22,19 +23,21 @@ interface Props { memo: Memo; } -export const getFormatedMemoCreatedAtStr = (createdTs: number, locale = "en"): string => { - if (Date.now() - createdTs < 1000 * 60 * 60 * 24) { - return dayjs(createdTs).locale(locale).fromNow(); +export const getFormatedMemoTimeStr = (time: number, locale = "en"): string => { + if (Date.now() - time < 1000 * 60 * 60 * 24) { + return dayjs(time).locale(locale).fromNow(); } 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) => { const memo = props.memo; + const user = useAppSelector((state) => state.user.user); const { t, i18n } = useTranslation(); const navigate = useNavigate(); - const [createdAtStr, setCreatedAtStr] = useState(getFormatedMemoCreatedAtStr(memo.createdTs, i18n.language)); + const [createdAtStr, setCreatedAtStr] = useState(getFormatedMemoTimeStr(memo.createdTs, i18n.language)); + const [updatedAtStr, setUpdatedAtStr] = useState(getFormatedMemoTimeStr(memo.updatedTs, i18n.language)); const memoContainerRef = useRef(null); const isVisitorMode = userService.isVisitorMode(); @@ -42,7 +45,8 @@ const Memo: React.FC = (props: Props) => { let intervalFlag: any = -1; if (Date.now() - memo.createdTs < 1000 * 60 * 60 * 24) { intervalFlag = setInterval(() => { - setCreatedAtStr(getFormatedMemoCreatedAtStr(memo.createdTs, i18n.language)); + setCreatedAtStr(getFormatedMemoTimeStr(memo.createdTs, i18n.language)); + setUpdatedAtStr(getFormatedMemoTimeStr(memo.updatedTs, i18n.language)); }, 1000 * 1); } @@ -182,12 +186,13 @@ const Memo: React.FC = (props: Props) => { editorStateService.setEditMemoWithId(memo.id); }; + const timeStr = user?.setting.memoSortOption === "created_ts" ? createdAtStr : `${t("common.update-on")} ${updatedAtStr}`; return (
- {createdAtStr} + {timeStr} {memo.visibility !== "PRIVATE" && !isVisitorMode && ( {memo.visibility} diff --git a/web/src/components/MemoEditor.tsx b/web/src/components/MemoEditor.tsx index d4c9e99d..3786f635 100644 --- a/web/src/components/MemoEditor.tsx +++ b/web/src/components/MemoEditor.tsx @@ -174,6 +174,7 @@ const MemoEditor: React.FC = () => { }); locationService.clearQuery(); } + locationService.setUpdatedFlag(); } catch (error: any) { console.error(error); toastHelper.error(error.response.data.message); diff --git a/web/src/components/MemoList.tsx b/web/src/components/MemoList.tsx index 702e2a08..83c384bf 100644 --- a/web/src/components/MemoList.tsx +++ b/web/src/components/MemoList.tsx @@ -12,6 +12,8 @@ import "../less/memo-list.less"; const MemoList = () => { const { t } = useTranslation(); 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 { tag: tagQuery, duration, type: memoType, text: textQuery, shortcutId } = query ?? {}; @@ -70,6 +72,11 @@ const MemoList = () => { const pinnedMemos = 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"); useEffect(() => { @@ -82,14 +89,14 @@ const MemoList = () => { console.error(error); toastHelper.error(error.response.data.message); }); - }, []); + }, [updatedTime]); useEffect(() => { const pageWrapper = document.body.querySelector(".page-wrapper"); if (pageWrapper) { pageWrapper.scrollTo(0, 0); } - }, [query]); + }, [query, updatedTime]); return (
diff --git a/web/src/components/Settings/PreferencesSection.tsx b/web/src/components/Settings/PreferencesSection.tsx index 24f86656..76f2cd69 100644 --- a/web/src/components/Settings/PreferencesSection.tsx +++ b/web/src/components/Settings/PreferencesSection.tsx @@ -1,7 +1,7 @@ import { useTranslation } from "react-i18next"; import { globalService, userService } from "../../services"; 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 "../../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) => { await userService.upsertUserSetting("locale", value); globalService.setLocale(value as Locale); @@ -69,6 +76,10 @@ const PreferencesSection = () => { await userService.upsertUserSetting("mobileEditorStyle", value); }; + const handleMemoSortOptionChanged = async (value: string) => { + await userService.upsertUserSetting("memoSortOption", value); + }; + return (

{t("common.basic")}

@@ -104,6 +115,15 @@ const PreferencesSection = () => { handleValueChanged={handleMobileEditorStyleChanged} /> +
); }; diff --git a/web/src/helpers/consts.ts b/web/src/helpers/consts.ts index 3c80989f..158a7c26 100644 --- a/web/src/helpers/consts.ts +++ b/web/src/helpers/consts.ts @@ -13,4 +13,9 @@ export const VISIBILITY_SELECTOR_ITEMS = [ { 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; diff --git a/web/src/locales/en.json b/web/src/locales/en.json index d780fbb3..f9db57a5 100644 --- a/web/src/locales/en.json +++ b/web/src/locales/en.json @@ -37,7 +37,8 @@ "tags": "Tags", "yourself": "Yourself", "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.", "auth": { @@ -128,7 +129,10 @@ "preference-section": { "default-memo-visibility": "Default memo visibility", "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": { "create-a-member": "Create a member" diff --git a/web/src/locales/vi.json b/web/src/locales/vi.json index 80638f42..98a7ca2a 100644 --- a/web/src/locales/vi.json +++ b/web/src/locales/vi.json @@ -37,7 +37,8 @@ "tags": "Thẻ", "yourself": "Chính bạn", "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.", "auth": { @@ -128,7 +129,10 @@ "preference-section": { "default-memo-visibility": "Chế độ memo mặc định", "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": { "create-a-member": "Tạo thành viên" diff --git a/web/src/locales/zh.json b/web/src/locales/zh.json index 5124f6e4..5e60f87f 100644 --- a/web/src/locales/zh.json +++ b/web/src/locales/zh.json @@ -37,7 +37,8 @@ "tags": "全部标签", "yourself": "你自己", "archived-at": "归档于", - "changed": "已更改" + "changed": "已更改", + "update-on": "更新于" }, "slogan": "一个开源的、支持私有化部署的碎片化知识卡片管理工具。", "auth": { @@ -128,7 +129,10 @@ "preference-section": { "default-memo-visibility": "默认 Memo 可见性", "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": { "create-a-member": "创建成员" diff --git a/web/src/services/locationService.ts b/web/src/services/locationService.ts index 6e75d5cd..bb165ca2 100644 --- a/web/src/services/locationService.ts +++ b/web/src/services/locationService.ts @@ -1,6 +1,7 @@ import { stringify } from "qs"; 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 { query, pathname, hash } = store.getState().location; @@ -112,6 +113,10 @@ const locationService = { ); updateLocationUrl(); }, + + setUpdatedFlag: () => { + store.dispatch(setUpdatedTime(getTimeStampByDate(new Date()).toString())); + }, }; export default locationService; diff --git a/web/src/services/userService.ts b/web/src/services/userService.ts index 68933eb2..0cca4262 100644 --- a/web/src/services/userService.ts +++ b/web/src/services/userService.ts @@ -8,6 +8,7 @@ const defauleSetting: Setting = { memoVisibility: "PRIVATE", editorFontStyle: "normal", mobileEditorStyle: "normal", + memoSortOption: "created_ts", }; export const convertResponseModelUser = (user: User): User => { diff --git a/web/src/store/modules/location.ts b/web/src/store/modules/location.ts index 7130f190..38b6e178 100644 --- a/web/src/store/modules/location.ts +++ b/web/src/store/modules/location.ts @@ -17,6 +17,7 @@ interface State { pathname: string; hash: string; query: Query; + updatedTime: string; } const getValidPathname = (pathname: string): string => { @@ -35,6 +36,7 @@ const getStateFromLocation = () => { pathname: getValidPathname(pathname), hash: hash, query: {}, + updatedTime: "", }; if (search !== "") { @@ -86,9 +88,15 @@ const locationSlice = createSlice({ }, }; }, + setUpdatedTime: (state, action: PayloadAction) => { + return { + ...state, + updatedTime: action.payload, + }; + }, }, }); -export const { setPathname, setQuery, updateStateWithLocation } = locationSlice.actions; +export const { setPathname, setQuery, setUpdatedTime, updateStateWithLocation } = locationSlice.actions; export default locationSlice.reducer; diff --git a/web/src/types/modules/setting.d.ts b/web/src/types/modules/setting.d.ts index e0d626b6..2e863618 100644 --- a/web/src/types/modules/setting.d.ts +++ b/web/src/types/modules/setting.d.ts @@ -3,6 +3,7 @@ interface Setting { memoVisibility: Visibility; editorFontStyle: "normal" | "mono"; mobileEditorStyle: "normal" | "float"; + memoSortOption: "created_ts" | "updated_ts"; } interface UserLocaleSetting {