mirror of
https://github.com/usememos/memos.git
synced 2025-03-17 11:10:07 +01:00
feat: pin/unpin memo
This commit is contained in:
parent
fcb5e2ee5a
commit
995ec34bf8
store/migration
web
public/icons
src
@ -31,7 +31,7 @@ CREATE TABLE memo (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
|
||||
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
|
||||
-- allowed row status are 'NORMAL', 'PINNED', 'HIDDEN'.
|
||||
-- allowed row status are 'NORMAL', 'ARCHIVED', 'HIDDEN'.
|
||||
row_status TEXT NOT NULL DEFAULT 'NORMAL',
|
||||
content TEXT NOT NULL DEFAULT '',
|
||||
creator_id INTEGER NOT NULL,
|
||||
@ -64,7 +64,7 @@ CREATE TABLE shortcut (
|
||||
title TEXT NOT NULL DEFAULT '',
|
||||
payload TEXT NOT NULL DEFAULT '',
|
||||
creator_id INTEGER NOT NULL,
|
||||
-- allowed row status are 'NORMAL', 'PINNED'.
|
||||
-- allowed row status are 'NORMAL', 'ARCHIVED'.
|
||||
row_status TEXT NOT NULL DEFAULT 'NORMAL',
|
||||
FOREIGN KEY(creator_id) REFERENCES users(id)
|
||||
);
|
||||
|
1
web/public/icons/add.svg
Normal file
1
web/public/icons/add.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="48" width="48"><path d="M22.5 38V25.5H10V22.5H22.5V10H25.5V22.5H38V25.5H25.5V38Z"/></svg>
|
After (image error) Size: 137 B |
@ -29,6 +29,26 @@ const Memo: React.FC<Props> = (props: Props) => {
|
||||
showMemoCardDialog(memo);
|
||||
};
|
||||
|
||||
const handleTogglePinMemoBtnClick = async () => {
|
||||
try {
|
||||
if (memo.rowStatus === "ARCHIVED") {
|
||||
await memoService.unpinMemo(memo.id);
|
||||
memoService.editMemo({
|
||||
...memo,
|
||||
rowStatus: "NORMAL",
|
||||
});
|
||||
} else {
|
||||
await memoService.pinMemo(memo.id);
|
||||
memoService.editMemo({
|
||||
...memo,
|
||||
rowStatus: "ARCHIVED",
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
// do nth
|
||||
}
|
||||
};
|
||||
|
||||
const handleMarkMemoClick = () => {
|
||||
globalStateService.setMarkMemoId(memo.id);
|
||||
};
|
||||
@ -86,6 +106,9 @@ const Memo: React.FC<Props> = (props: Props) => {
|
||||
<div className="memo-top-wrapper">
|
||||
<span className="time-text" onClick={handleShowMemoStoryDialog}>
|
||||
{memo.createdAtStr}
|
||||
<Only when={memo.rowStatus === "ARCHIVED"}>
|
||||
<span className="ml-2">PINNED</span>
|
||||
</Only>
|
||||
</span>
|
||||
<div className="btns-container">
|
||||
<span className="btn more-action-btn">
|
||||
@ -96,6 +119,9 @@ const Memo: React.FC<Props> = (props: Props) => {
|
||||
<span className="btn" onClick={handleShowMemoStoryDialog}>
|
||||
View Story
|
||||
</span>
|
||||
<span className="btn" onClick={handleTogglePinMemoBtnClick}>
|
||||
{memo.rowStatus === "NORMAL" ? "Pin" : "Unpin"}
|
||||
</span>
|
||||
<span className="btn" onClick={handleMarkMemoClick}>
|
||||
Mark
|
||||
</span>
|
||||
|
@ -50,7 +50,7 @@ const MemoFilter: React.FC<FilterProps> = () => {
|
||||
locationService.setFromAndToQuery(0, 0);
|
||||
}}
|
||||
>
|
||||
<span className="icon-text">🗓️</span> {utils.getDateString(duration.from)} 至 {utils.getDateString(duration.to)}
|
||||
<span className="icon-text">🗓️</span> {utils.getDateString(duration.from)} to {utils.getDateString(duration.to)}
|
||||
</div>
|
||||
) : null}
|
||||
<div
|
||||
|
@ -76,6 +76,10 @@ const MemoList: React.FC<Props> = () => {
|
||||
})
|
||||
: memos;
|
||||
|
||||
const pinnedMemos = shownMemos.filter((m) => m.rowStatus === "ARCHIVED");
|
||||
const unpinnedMemos = shownMemos.filter((m) => m.rowStatus === "NORMAL");
|
||||
const sortedMemos = pinnedMemos.concat(unpinnedMemos);
|
||||
|
||||
useEffect(() => {
|
||||
memoService
|
||||
.fetchAllMemos()
|
||||
@ -84,7 +88,7 @@ const MemoList: React.FC<Props> = () => {
|
||||
memoService.updateTagsState();
|
||||
})
|
||||
.catch(() => {
|
||||
toastHelper.error("😭 Refresh failed, please try again later.");
|
||||
toastHelper.error("😭 Fetching failed, please try again later.");
|
||||
});
|
||||
}, []);
|
||||
|
||||
@ -107,14 +111,14 @@ const MemoList: React.FC<Props> = () => {
|
||||
|
||||
return (
|
||||
<div className={`memo-list-container ${isFetching ? "" : "completed"}`} onClick={handleMemoListClick} ref={wrapperElement}>
|
||||
{shownMemos.map((memo) => (
|
||||
{sortedMemos.map((memo) => (
|
||||
<Memo key={`${memo.id}-${memo.updatedAt}`} memo={memo} />
|
||||
))}
|
||||
<div className="status-text-container">
|
||||
<p className="status-text">
|
||||
{isFetching
|
||||
? "Fetching data..."
|
||||
: shownMemos.length === 0
|
||||
: sortedMemos.length === 0
|
||||
? "Oops, there is nothing"
|
||||
: showMemoFilter
|
||||
? ""
|
||||
|
@ -1,12 +1,12 @@
|
||||
import { useContext, useEffect } from "react";
|
||||
import { locationService, shortcutService } from "../services";
|
||||
import appContext from "../stores/appContext";
|
||||
import useToggle from "../hooks/useToggle";
|
||||
import useLoading from "../hooks/useLoading";
|
||||
import Only from "./common/OnlyWhen";
|
||||
import utils from "../helpers/utils";
|
||||
import Only from "./common/OnlyWhen";
|
||||
import toastHelper from "./Toast";
|
||||
import { locationService, shortcutService } from "../services";
|
||||
import showCreateQueryDialog from "./CreateShortcutDialog";
|
||||
import showCreateShortcutDialog from "./CreateShortcutDialog";
|
||||
import "../less/shortcut-list.less";
|
||||
|
||||
interface Props {}
|
||||
@ -19,9 +19,13 @@ const ShortcutList: React.FC<Props> = () => {
|
||||
},
|
||||
} = useContext(appContext);
|
||||
const loadingState = useLoading();
|
||||
const sortedShortcuts = shortcuts
|
||||
.sort((a, b) => utils.getTimeStampByDate(b.createdAt) - utils.getTimeStampByDate(a.createdAt))
|
||||
.sort((a, b) => utils.getTimeStampByDate(b.updatedAt) - utils.getTimeStampByDate(a.updatedAt));
|
||||
const pinnedShortcuts = shortcuts
|
||||
.filter((s) => s.rowStatus === "ARCHIVED")
|
||||
.sort((a, b) => utils.getTimeStampByDate(b.createdAt) - utils.getTimeStampByDate(a.createdAt));
|
||||
const unpinnedShortcuts = shortcuts
|
||||
.filter((s) => s.rowStatus === "NORMAL")
|
||||
.sort((a, b) => utils.getTimeStampByDate(b.createdAt) - utils.getTimeStampByDate(a.createdAt));
|
||||
const sortedShortcuts = pinnedShortcuts.concat(unpinnedShortcuts);
|
||||
|
||||
useEffect(() => {
|
||||
shortcutService
|
||||
@ -38,13 +42,13 @@ const ShortcutList: React.FC<Props> = () => {
|
||||
<div className="shortcuts-wrapper">
|
||||
<p className="title-text">
|
||||
<span className="normal-text">Shortcuts</span>
|
||||
<span className="btn" onClick={() => showCreateQueryDialog()}>
|
||||
+
|
||||
<span className="btn" onClick={() => showCreateShortcutDialog()}>
|
||||
<img src="/icons/add.svg" alt="add shortcut" />
|
||||
</span>
|
||||
</p>
|
||||
<Only when={loadingState.isSucceed && sortedShortcuts.length === 0}>
|
||||
<div className="create-shortcut-btn-container">
|
||||
<span className="btn" onClick={() => showCreateQueryDialog()}>
|
||||
<span className="btn" onClick={() => showCreateShortcutDialog()}>
|
||||
New shortcut
|
||||
</span>
|
||||
</div>
|
||||
@ -92,12 +96,12 @@ const ShortcutContainer: React.FC<ShortcutContainerProps> = (props: ShortcutCont
|
||||
}
|
||||
};
|
||||
|
||||
const handleEditQueryBtnClick = (event: React.MouseEvent) => {
|
||||
const handleEditShortcutBtnClick = (event: React.MouseEvent) => {
|
||||
event.stopPropagation();
|
||||
showCreateQueryDialog(shortcut.id);
|
||||
showCreateShortcutDialog(shortcut.id);
|
||||
};
|
||||
|
||||
const handlePinQueryBtnClick = async (event: React.MouseEvent) => {
|
||||
const handlePinShortcutBtnClick = async (event: React.MouseEvent) => {
|
||||
event.stopPropagation();
|
||||
|
||||
try {
|
||||
@ -136,10 +140,10 @@ const ShortcutContainer: React.FC<ShortcutContainerProps> = (props: ShortcutCont
|
||||
</span>
|
||||
<div className="action-btns-wrapper">
|
||||
<div className="action-btns-container">
|
||||
<span className="btn" onClick={handlePinQueryBtnClick}>
|
||||
<span className="btn" onClick={handlePinShortcutBtnClick}>
|
||||
{shortcut.rowStatus === "ARCHIVED" ? "Unpin" : "Pin"}
|
||||
</span>
|
||||
<span className="btn" onClick={handleEditQueryBtnClick}>
|
||||
<span className="btn" onClick={handleEditShortcutBtnClick}>
|
||||
Edit
|
||||
</span>
|
||||
<span
|
||||
|
@ -125,7 +125,7 @@ const UsageHeatMap: React.FC<Props> = () => {
|
||||
></span>
|
||||
);
|
||||
})}
|
||||
{nullCell.map((v, i) => (
|
||||
{nullCell.map((_, i) => (
|
||||
<span className="stat-container null" key={i}></span>
|
||||
))}
|
||||
</div>
|
||||
|
@ -113,7 +113,7 @@ namespace api {
|
||||
export function getMyMemos() {
|
||||
return request<Model.Memo[]>({
|
||||
method: "GET",
|
||||
url: "/api/memo?rowStatus=NORMAL",
|
||||
url: "/api/memo",
|
||||
});
|
||||
}
|
||||
|
||||
@ -144,6 +144,26 @@ namespace api {
|
||||
});
|
||||
}
|
||||
|
||||
export function pinMemo(memoId: string) {
|
||||
return request({
|
||||
method: "PATCH",
|
||||
url: `/api/memo/${memoId}`,
|
||||
data: {
|
||||
rowStatus: "ARCHIVED",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function unpinMemo(shortcutId: string) {
|
||||
return request({
|
||||
method: "PATCH",
|
||||
url: `/api/memo/${shortcutId}`,
|
||||
data: {
|
||||
rowStatus: "NORMAL",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function hideMemo(memoId: string) {
|
||||
return request({
|
||||
method: "PATCH",
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
.filter-query-container {
|
||||
.flex(row, flex-start, flex-start);
|
||||
@apply w-full flex-wrap p-2 pb-1 text-sm leading-7;
|
||||
@apply w-full flex-wrap p-2 pb-1 text-sm font-mono leading-7;
|
||||
|
||||
> .tip-text {
|
||||
@apply mr-2;
|
||||
@ -10,10 +10,6 @@
|
||||
|
||||
> .filter-item-container {
|
||||
@apply px-2 mr-2 cursor-pointer bg-gray-200 rounded whitespace-nowrap truncate hover:line-through;
|
||||
max-width: 200px;
|
||||
|
||||
> .icon-text {
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
max-width: 256px;
|
||||
}
|
||||
}
|
||||
|
@ -28,6 +28,6 @@
|
||||
}
|
||||
|
||||
&.completed {
|
||||
@apply pb-28;
|
||||
@apply pb-40;
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,7 @@
|
||||
|
||||
.memo-wrapper {
|
||||
.flex(column, flex-start, flex-start);
|
||||
@apply w-full max-w-full p-4 px-6 mt-2 first:mt-2 bg-white rounded-lg border border-transparent hover:border-gray-200;
|
||||
@apply w-full max-w-full p-4 pb-3 mt-2 bg-white rounded-lg border border-transparent hover:border-gray-200;
|
||||
|
||||
&.deleted-memo {
|
||||
@apply border-gray-200;
|
||||
@ -11,7 +11,7 @@
|
||||
|
||||
> .memo-top-wrapper {
|
||||
.flex(row, space-between, center);
|
||||
@apply w-full h-6 mb-1;
|
||||
@apply w-full h-6 mb-2;
|
||||
|
||||
> .time-text {
|
||||
@apply text-xs text-gray-400 cursor-pointer;
|
||||
|
@ -6,28 +6,26 @@
|
||||
.hide-scroll-bar();
|
||||
|
||||
> .title-text {
|
||||
.flex(row, space-between, center);
|
||||
.flex(row, flex-start, center);
|
||||
@apply w-full px-4;
|
||||
|
||||
> .normal-text {
|
||||
@apply text-xs leading-6 font-bold text-black opacity-50;
|
||||
@apply text-xs leading-6 font-mono text-gray-400;
|
||||
}
|
||||
|
||||
> .btn {
|
||||
@apply hidden px-1 text-lg leading-6;
|
||||
}
|
||||
.flex(column, center, center);
|
||||
@apply w-5 h-5 bg-gray-200 rounded ml-2 hover:opacity-80;
|
||||
|
||||
&:hover,
|
||||
&:active {
|
||||
> .btn {
|
||||
@apply block;
|
||||
> img {
|
||||
@apply w-4 h-4 opacity-80;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .create-shortcut-btn-container {
|
||||
.flex(row, center, center);
|
||||
@apply w-full mt-4 mb-2;
|
||||
.flex(row, flex-start, center);
|
||||
@apply w-full mt-4 mb-2 ml-4;
|
||||
|
||||
> .btn {
|
||||
@apply flex p-2 px-4 rounded-lg text-sm border border-dashed border-blue-600;
|
||||
|
@ -42,7 +42,7 @@ class LocationService {
|
||||
state.query.tag = urlParams.get("tag") ?? "";
|
||||
state.query.type = (urlParams.get("type") ?? "") as MemoSpecType;
|
||||
state.query.text = urlParams.get("text") ?? "";
|
||||
state.query.shortcutId = urlParams.get("filter") ?? "";
|
||||
state.query.shortcutId = urlParams.get("shortcutId") ?? "";
|
||||
const from = parseInt(urlParams.get("from") ?? "0");
|
||||
const to = parseInt(urlParams.get("to") ?? "0");
|
||||
if (to > from && to !== 0) {
|
||||
|
@ -17,7 +17,7 @@ class MemoService {
|
||||
}
|
||||
|
||||
const data = await api.getMyMemos();
|
||||
const memos: Model.Memo[] = data.map((m) => this.convertResponseModelMemo(m));
|
||||
const memos: Model.Memo[] = data.filter((m) => m.rowStatus !== "HIDDEN").map((m) => this.convertResponseModelMemo(m));
|
||||
appStore.dispatch({
|
||||
type: "SET_MEMOS",
|
||||
payload: {
|
||||
@ -133,6 +133,14 @@ class MemoService {
|
||||
return this.convertResponseModelMemo(memo);
|
||||
}
|
||||
|
||||
public async pinMemo(memoId: string) {
|
||||
await api.pinMemo(memoId);
|
||||
}
|
||||
|
||||
public async unpinMemo(memoId: string) {
|
||||
await api.unpinMemo(memoId);
|
||||
}
|
||||
|
||||
private convertResponseModelMemo(memo: Model.Memo): Model.Memo {
|
||||
return {
|
||||
...memo,
|
||||
|
2
web/src/types/models.d.ts
vendored
2
web/src/types/models.d.ts
vendored
@ -15,7 +15,7 @@ declare namespace Model {
|
||||
|
||||
interface Memo extends BaseModel {
|
||||
content: string;
|
||||
rowStatus: "NORMAL" | "HIDDEN";
|
||||
rowStatus: "NORMAL" | "ARCHIVED" | "HIDDEN";
|
||||
}
|
||||
|
||||
interface Shortcut extends BaseModel {
|
||||
|
Loading…
x
Reference in New Issue
Block a user