mirror of
https://github.com/usememos/memos.git
synced 2025-06-05 22:09:59 +02:00
feat: pin/unpin memo
This commit is contained in:
@ -31,7 +31,7 @@ CREATE TABLE memo (
|
|||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
|
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
|
||||||
updated_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',
|
row_status TEXT NOT NULL DEFAULT 'NORMAL',
|
||||||
content TEXT NOT NULL DEFAULT '',
|
content TEXT NOT NULL DEFAULT '',
|
||||||
creator_id INTEGER NOT NULL,
|
creator_id INTEGER NOT NULL,
|
||||||
@ -64,7 +64,7 @@ CREATE TABLE shortcut (
|
|||||||
title TEXT NOT NULL DEFAULT '',
|
title TEXT NOT NULL DEFAULT '',
|
||||||
payload TEXT NOT NULL DEFAULT '',
|
payload TEXT NOT NULL DEFAULT '',
|
||||||
creator_id INTEGER NOT NULL,
|
creator_id INTEGER NOT NULL,
|
||||||
-- allowed row status are 'NORMAL', 'PINNED'.
|
-- allowed row status are 'NORMAL', 'ARCHIVED'.
|
||||||
row_status TEXT NOT NULL DEFAULT 'NORMAL',
|
row_status TEXT NOT NULL DEFAULT 'NORMAL',
|
||||||
FOREIGN KEY(creator_id) REFERENCES users(id)
|
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 Width: | Height: | Size: 137 B |
@ -29,6 +29,26 @@ const Memo: React.FC<Props> = (props: Props) => {
|
|||||||
showMemoCardDialog(memo);
|
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 = () => {
|
const handleMarkMemoClick = () => {
|
||||||
globalStateService.setMarkMemoId(memo.id);
|
globalStateService.setMarkMemoId(memo.id);
|
||||||
};
|
};
|
||||||
@ -86,6 +106,9 @@ const Memo: React.FC<Props> = (props: Props) => {
|
|||||||
<div className="memo-top-wrapper">
|
<div className="memo-top-wrapper">
|
||||||
<span className="time-text" onClick={handleShowMemoStoryDialog}>
|
<span className="time-text" onClick={handleShowMemoStoryDialog}>
|
||||||
{memo.createdAtStr}
|
{memo.createdAtStr}
|
||||||
|
<Only when={memo.rowStatus === "ARCHIVED"}>
|
||||||
|
<span className="ml-2">PINNED</span>
|
||||||
|
</Only>
|
||||||
</span>
|
</span>
|
||||||
<div className="btns-container">
|
<div className="btns-container">
|
||||||
<span className="btn more-action-btn">
|
<span className="btn more-action-btn">
|
||||||
@ -96,6 +119,9 @@ const Memo: React.FC<Props> = (props: Props) => {
|
|||||||
<span className="btn" onClick={handleShowMemoStoryDialog}>
|
<span className="btn" onClick={handleShowMemoStoryDialog}>
|
||||||
View Story
|
View Story
|
||||||
</span>
|
</span>
|
||||||
|
<span className="btn" onClick={handleTogglePinMemoBtnClick}>
|
||||||
|
{memo.rowStatus === "NORMAL" ? "Pin" : "Unpin"}
|
||||||
|
</span>
|
||||||
<span className="btn" onClick={handleMarkMemoClick}>
|
<span className="btn" onClick={handleMarkMemoClick}>
|
||||||
Mark
|
Mark
|
||||||
</span>
|
</span>
|
||||||
|
@ -50,7 +50,7 @@ const MemoFilter: React.FC<FilterProps> = () => {
|
|||||||
locationService.setFromAndToQuery(0, 0);
|
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>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
<div
|
<div
|
||||||
|
@ -76,6 +76,10 @@ const MemoList: React.FC<Props> = () => {
|
|||||||
})
|
})
|
||||||
: memos;
|
: memos;
|
||||||
|
|
||||||
|
const pinnedMemos = shownMemos.filter((m) => m.rowStatus === "ARCHIVED");
|
||||||
|
const unpinnedMemos = shownMemos.filter((m) => m.rowStatus === "NORMAL");
|
||||||
|
const sortedMemos = pinnedMemos.concat(unpinnedMemos);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
memoService
|
memoService
|
||||||
.fetchAllMemos()
|
.fetchAllMemos()
|
||||||
@ -84,7 +88,7 @@ const MemoList: React.FC<Props> = () => {
|
|||||||
memoService.updateTagsState();
|
memoService.updateTagsState();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.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 (
|
return (
|
||||||
<div className={`memo-list-container ${isFetching ? "" : "completed"}`} onClick={handleMemoListClick} ref={wrapperElement}>
|
<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} />
|
<Memo key={`${memo.id}-${memo.updatedAt}`} memo={memo} />
|
||||||
))}
|
))}
|
||||||
<div className="status-text-container">
|
<div className="status-text-container">
|
||||||
<p className="status-text">
|
<p className="status-text">
|
||||||
{isFetching
|
{isFetching
|
||||||
? "Fetching data..."
|
? "Fetching data..."
|
||||||
: shownMemos.length === 0
|
: sortedMemos.length === 0
|
||||||
? "Oops, there is nothing"
|
? "Oops, there is nothing"
|
||||||
: showMemoFilter
|
: showMemoFilter
|
||||||
? ""
|
? ""
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
import { useContext, useEffect } from "react";
|
import { useContext, useEffect } from "react";
|
||||||
|
import { locationService, shortcutService } from "../services";
|
||||||
import appContext from "../stores/appContext";
|
import appContext from "../stores/appContext";
|
||||||
import useToggle from "../hooks/useToggle";
|
import useToggle from "../hooks/useToggle";
|
||||||
import useLoading from "../hooks/useLoading";
|
import useLoading from "../hooks/useLoading";
|
||||||
import Only from "./common/OnlyWhen";
|
|
||||||
import utils from "../helpers/utils";
|
import utils from "../helpers/utils";
|
||||||
|
import Only from "./common/OnlyWhen";
|
||||||
import toastHelper from "./Toast";
|
import toastHelper from "./Toast";
|
||||||
import { locationService, shortcutService } from "../services";
|
import showCreateShortcutDialog from "./CreateShortcutDialog";
|
||||||
import showCreateQueryDialog from "./CreateShortcutDialog";
|
|
||||||
import "../less/shortcut-list.less";
|
import "../less/shortcut-list.less";
|
||||||
|
|
||||||
interface Props {}
|
interface Props {}
|
||||||
@ -19,9 +19,13 @@ const ShortcutList: React.FC<Props> = () => {
|
|||||||
},
|
},
|
||||||
} = useContext(appContext);
|
} = useContext(appContext);
|
||||||
const loadingState = useLoading();
|
const loadingState = useLoading();
|
||||||
const sortedShortcuts = shortcuts
|
const pinnedShortcuts = shortcuts
|
||||||
.sort((a, b) => utils.getTimeStampByDate(b.createdAt) - utils.getTimeStampByDate(a.createdAt))
|
.filter((s) => s.rowStatus === "ARCHIVED")
|
||||||
.sort((a, b) => utils.getTimeStampByDate(b.updatedAt) - utils.getTimeStampByDate(a.updatedAt));
|
.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(() => {
|
useEffect(() => {
|
||||||
shortcutService
|
shortcutService
|
||||||
@ -38,13 +42,13 @@ const ShortcutList: React.FC<Props> = () => {
|
|||||||
<div className="shortcuts-wrapper">
|
<div className="shortcuts-wrapper">
|
||||||
<p className="title-text">
|
<p className="title-text">
|
||||||
<span className="normal-text">Shortcuts</span>
|
<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>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
<Only when={loadingState.isSucceed && sortedShortcuts.length === 0}>
|
<Only when={loadingState.isSucceed && sortedShortcuts.length === 0}>
|
||||||
<div className="create-shortcut-btn-container">
|
<div className="create-shortcut-btn-container">
|
||||||
<span className="btn" onClick={() => showCreateQueryDialog()}>
|
<span className="btn" onClick={() => showCreateShortcutDialog()}>
|
||||||
New shortcut
|
New shortcut
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@ -92,12 +96,12 @@ const ShortcutContainer: React.FC<ShortcutContainerProps> = (props: ShortcutCont
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEditQueryBtnClick = (event: React.MouseEvent) => {
|
const handleEditShortcutBtnClick = (event: React.MouseEvent) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
showCreateQueryDialog(shortcut.id);
|
showCreateShortcutDialog(shortcut.id);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePinQueryBtnClick = async (event: React.MouseEvent) => {
|
const handlePinShortcutBtnClick = async (event: React.MouseEvent) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -136,10 +140,10 @@ const ShortcutContainer: React.FC<ShortcutContainerProps> = (props: ShortcutCont
|
|||||||
</span>
|
</span>
|
||||||
<div className="action-btns-wrapper">
|
<div className="action-btns-wrapper">
|
||||||
<div className="action-btns-container">
|
<div className="action-btns-container">
|
||||||
<span className="btn" onClick={handlePinQueryBtnClick}>
|
<span className="btn" onClick={handlePinShortcutBtnClick}>
|
||||||
{shortcut.rowStatus === "ARCHIVED" ? "Unpin" : "Pin"}
|
{shortcut.rowStatus === "ARCHIVED" ? "Unpin" : "Pin"}
|
||||||
</span>
|
</span>
|
||||||
<span className="btn" onClick={handleEditQueryBtnClick}>
|
<span className="btn" onClick={handleEditShortcutBtnClick}>
|
||||||
Edit
|
Edit
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
|
@ -125,7 +125,7 @@ const UsageHeatMap: React.FC<Props> = () => {
|
|||||||
></span>
|
></span>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{nullCell.map((v, i) => (
|
{nullCell.map((_, i) => (
|
||||||
<span className="stat-container null" key={i}></span>
|
<span className="stat-container null" key={i}></span>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
@ -113,7 +113,7 @@ namespace api {
|
|||||||
export function getMyMemos() {
|
export function getMyMemos() {
|
||||||
return request<Model.Memo[]>({
|
return request<Model.Memo[]>({
|
||||||
method: "GET",
|
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) {
|
export function hideMemo(memoId: string) {
|
||||||
return request({
|
return request({
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
.filter-query-container {
|
.filter-query-container {
|
||||||
.flex(row, flex-start, flex-start);
|
.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 {
|
> .tip-text {
|
||||||
@apply mr-2;
|
@apply mr-2;
|
||||||
@ -10,10 +10,6 @@
|
|||||||
|
|
||||||
> .filter-item-container {
|
> .filter-item-container {
|
||||||
@apply px-2 mr-2 cursor-pointer bg-gray-200 rounded whitespace-nowrap truncate hover:line-through;
|
@apply px-2 mr-2 cursor-pointer bg-gray-200 rounded whitespace-nowrap truncate hover:line-through;
|
||||||
max-width: 200px;
|
max-width: 256px;
|
||||||
|
|
||||||
> .icon-text {
|
|
||||||
letter-spacing: 2px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -28,6 +28,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&.completed {
|
&.completed {
|
||||||
@apply pb-28;
|
@apply pb-40;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
.memo-wrapper {
|
.memo-wrapper {
|
||||||
.flex(column, flex-start, flex-start);
|
.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 {
|
&.deleted-memo {
|
||||||
@apply border-gray-200;
|
@apply border-gray-200;
|
||||||
@ -11,7 +11,7 @@
|
|||||||
|
|
||||||
> .memo-top-wrapper {
|
> .memo-top-wrapper {
|
||||||
.flex(row, space-between, center);
|
.flex(row, space-between, center);
|
||||||
@apply w-full h-6 mb-1;
|
@apply w-full h-6 mb-2;
|
||||||
|
|
||||||
> .time-text {
|
> .time-text {
|
||||||
@apply text-xs text-gray-400 cursor-pointer;
|
@apply text-xs text-gray-400 cursor-pointer;
|
||||||
|
@ -6,28 +6,26 @@
|
|||||||
.hide-scroll-bar();
|
.hide-scroll-bar();
|
||||||
|
|
||||||
> .title-text {
|
> .title-text {
|
||||||
.flex(row, space-between, center);
|
.flex(row, flex-start, center);
|
||||||
@apply w-full px-4;
|
@apply w-full px-4;
|
||||||
|
|
||||||
> .normal-text {
|
> .normal-text {
|
||||||
@apply text-xs leading-6 font-bold text-black opacity-50;
|
@apply text-xs leading-6 font-mono text-gray-400;
|
||||||
}
|
}
|
||||||
|
|
||||||
> .btn {
|
> .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,
|
> img {
|
||||||
&:active {
|
@apply w-4 h-4 opacity-80;
|
||||||
> .btn {
|
|
||||||
@apply block;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
> .create-shortcut-btn-container {
|
> .create-shortcut-btn-container {
|
||||||
.flex(row, center, center);
|
.flex(row, flex-start, center);
|
||||||
@apply w-full mt-4 mb-2;
|
@apply w-full mt-4 mb-2 ml-4;
|
||||||
|
|
||||||
> .btn {
|
> .btn {
|
||||||
@apply flex p-2 px-4 rounded-lg text-sm border border-dashed border-blue-600;
|
@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.tag = urlParams.get("tag") ?? "";
|
||||||
state.query.type = (urlParams.get("type") ?? "") as MemoSpecType;
|
state.query.type = (urlParams.get("type") ?? "") as MemoSpecType;
|
||||||
state.query.text = urlParams.get("text") ?? "";
|
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 from = parseInt(urlParams.get("from") ?? "0");
|
||||||
const to = parseInt(urlParams.get("to") ?? "0");
|
const to = parseInt(urlParams.get("to") ?? "0");
|
||||||
if (to > from && to !== 0) {
|
if (to > from && to !== 0) {
|
||||||
|
@ -17,7 +17,7 @@ class MemoService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const data = await api.getMyMemos();
|
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({
|
appStore.dispatch({
|
||||||
type: "SET_MEMOS",
|
type: "SET_MEMOS",
|
||||||
payload: {
|
payload: {
|
||||||
@ -133,6 +133,14 @@ class MemoService {
|
|||||||
return this.convertResponseModelMemo(memo);
|
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 {
|
private convertResponseModelMemo(memo: Model.Memo): Model.Memo {
|
||||||
return {
|
return {
|
||||||
...memo,
|
...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 {
|
interface Memo extends BaseModel {
|
||||||
content: string;
|
content: string;
|
||||||
rowStatus: "NORMAL" | "HIDDEN";
|
rowStatus: "NORMAL" | "ARCHIVED" | "HIDDEN";
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Shortcut extends BaseModel {
|
interface Shortcut extends BaseModel {
|
||||||
|
Reference in New Issue
Block a user