feat: pin/unpin memo

This commit is contained in:
boojack 2022-05-02 10:57:20 +08:00
parent fcb5e2ee5a
commit 995ec34bf8
15 changed files with 101 additions and 44 deletions

View file

@ -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
View 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

View file

@ -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>

View file

@ -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

View file

@ -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
? ""

View file

@ -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

View file

@ -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>

View file

@ -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",

View file

@ -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;
}
}

View file

@ -28,6 +28,6 @@
}
&.completed {
@apply pb-28;
@apply pb-40;
}
}

View file

@ -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;

View file

@ -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;

View file

@ -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) {

View file

@ -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,

View file

@ -15,7 +15,7 @@ declare namespace Model {
interface Memo extends BaseModel {
content: string;
rowStatus: "NORMAL" | "HIDDEN";
rowStatus: "NORMAL" | "ARCHIVED" | "HIDDEN";
}
interface Shortcut extends BaseModel {