feat: pin/unpin memo

pull/49/head
boojack 3 years ago
parent fcb5e2ee5a
commit 995ec34bf8

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

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

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

Loading…
Cancel
Save