From 615cec3066cdd8ae2ef5bfee94afd065b12001cc Mon Sep 17 00:00:00 2001 From: STEVEN Date: Sun, 15 May 2022 22:21:13 +0800 Subject: [PATCH] feat: import data from json (#53) --- api/memo.go | 2 + store/memo.go | 16 +++++--- web/src/components/AboutSiteDialog.tsx | 5 +-- web/src/components/ChangePasswordDialog.tsx | 6 +-- web/src/components/MyAccountSection.tsx | 12 ++---- web/src/components/PreferencesSection.tsx | 43 ++++++++++++++++++++- web/src/helpers/api.ts | 17 ++++++-- web/src/less/change-password-dialog.less | 8 ---- web/src/less/menu-btns-popup.less | 2 +- web/src/less/my-account-section.less | 24 ++---------- web/src/less/setting-dialog.less | 4 +- web/src/services/memoService.ts | 12 ++++-- 12 files changed, 90 insertions(+), 61 deletions(-) diff --git a/api/memo.go b/api/memo.go index d5ba9290..ebfce6c2 100644 --- a/api/memo.go +++ b/api/memo.go @@ -16,6 +16,8 @@ type Memo struct { type MemoCreate struct { // Standard fields CreatorID int + // Used to import memos with clearly created ts. + CreatedTs *int64 `json:"createdTs"` // Domain specific fields Content string `json:"content"` diff --git a/store/memo.go b/store/memo.go index 22472329..3f2b5840 100644 --- a/store/memo.go +++ b/store/memo.go @@ -65,16 +65,22 @@ func (s *MemoService) DeleteMemo(delete *api.MemoDelete) error { } func createMemo(db *DB, create *api.MemoCreate) (*api.Memo, error) { + set := []string{"creator_id", "content"} + placeholder := []string{"?", "?"} + args := []interface{}{create.CreatorID, create.Content} + + if v := create.CreatedTs; v != nil { + set, placeholder, args = append(set, "created_ts"), append(placeholder, "?"), append(args, *v) + } + row, err := db.Db.Query(` INSERT INTO memo ( - creator_id, - content + `+strings.Join(set, ", ")+` ) - VALUES (?, ?) + VALUES (`+strings.Join(placeholder, ",")+`) RETURNING id, creator_id, created_ts, updated_ts, content, row_status `, - create.CreatorID, - create.Content, + args..., ) if err != nil { return nil, FormatError(err) diff --git a/web/src/components/AboutSiteDialog.tsx b/web/src/components/AboutSiteDialog.tsx index d0e95618..aa20f1ca 100644 --- a/web/src/components/AboutSiteDialog.tsx +++ b/web/src/components/AboutSiteDialog.tsx @@ -35,13 +35,12 @@ const AboutSiteDialog: React.FC = ({ destroy }: Props) => {

- Memos is an open source, self-hosted alternative to flomo. + Memos is an open source, quickly self-hosted alternative flomo.

-

Built with `Golang` and `React`.


🏗 This project is working in progress, and very pleasure to your{" "} - issues. + PRs.

Last updated on {lastUpdatedAt} 🎉 diff --git a/web/src/components/ChangePasswordDialog.tsx b/web/src/components/ChangePasswordDialog.tsx index 8df6437e..3fc98105 100644 --- a/web/src/components/ChangePasswordDialog.tsx +++ b/web/src/components/ChangePasswordDialog.tsx @@ -73,12 +73,10 @@ const ChangePasswordDialog: React.FC = ({ destroy }: Props) => {

diff --git a/web/src/components/MyAccountSection.tsx b/web/src/components/MyAccountSection.tsx index ce2d18d2..066a73b2 100644 --- a/web/src/components/MyAccountSection.tsx +++ b/web/src/components/MyAccountSection.tsx @@ -66,17 +66,13 @@ const MyAccountSection: React.FC = () => {

Account Information

-
-

Open API (Experimental feature)

+

Open API

{openAPIRoute}

Reset API diff --git a/web/src/components/PreferencesSection.tsx b/web/src/components/PreferencesSection.tsx index f4f1a6fc..b767146a 100644 --- a/web/src/components/PreferencesSection.tsx +++ b/web/src/components/PreferencesSection.tsx @@ -1,7 +1,9 @@ import { useContext } from "react"; import appContext from "../stores/appContext"; import { globalStateService, memoService } from "../services"; +import utils from "../helpers/utils"; import { formatMemoContent } from "./Memo"; +import toastHelper from "./Toast"; import "../less/preferences-section.less"; interface Props {} @@ -41,13 +43,49 @@ const PreferencesSection: React.FC = () => { const jsonStr = JSON.stringify(formatedMemos); const element = document.createElement("a"); element.setAttribute("href", "data:text/json;charset=utf-8," + encodeURIComponent(jsonStr)); - element.setAttribute("download", "data.json"); + element.setAttribute("download", `memos-${utils.getDateTimeString(Date.now())}.json`); element.style.display = "none"; document.body.appendChild(element); element.click(); document.body.removeChild(element); }; + const handleImportBtnClick = async () => { + const fileInputEl = document.createElement("input"); + fileInputEl.type = "file"; + fileInputEl.accept = "application/JSON"; + fileInputEl.onchange = () => { + if (fileInputEl.files?.length && fileInputEl.files.length > 0) { + const reader = new FileReader(); + reader.readAsText(fileInputEl.files[0]); + reader.onload = async (event) => { + const memoList = JSON.parse(event.target?.result as string) as Model.Memo[]; + if (!Array.isArray(memoList)) { + toastHelper.error("Unexpected data type."); + } + + let succeedAmount = 0; + + for (const memo of memoList) { + const content = memo.content || ""; + const createdAt = memo.createdAt || utils.getDateTimeString(Date.now()); + + try { + await memoService.importMemo(content, createdAt); + succeedAmount++; + } catch (error) { + // do nth + } + } + + await memoService.fetchAllMemos(); + toastHelper.success(`${succeedAmount} memos successfully imported.`); + }; + } + }; + fileInputEl.click(); + }; + return ( <>
@@ -75,6 +113,9 @@ const PreferencesSection: React.FC = () => { +
diff --git a/web/src/helpers/api.ts b/web/src/helpers/api.ts index ef91edd3..5e9a9c6c 100644 --- a/web/src/helpers/api.ts +++ b/web/src/helpers/api.ts @@ -1,3 +1,5 @@ +import utils from "./utils"; + type ResponseObject = { data: T; error?: string; @@ -133,13 +135,20 @@ namespace api { }); } - export function createMemo(content: string) { + export function createMemo(content: string, createdAt?: string) { + const data: any = { + content, + }; + + if (createdAt) { + const createdTms = utils.getTimeStampByDate(createdAt); + data.createdTs = Math.floor(createdTms / 1000); + } + return request({ method: "POST", url: "/api/memo", - data: { - content, - }, + data: data, }); } diff --git a/web/src/less/change-password-dialog.less b/web/src/less/change-password-dialog.less index b7e7e227..a1b9050f 100644 --- a/web/src/less/change-password-dialog.less +++ b/web/src/less/change-password-dialog.less @@ -15,14 +15,6 @@ .flex(column, flex-start, flex-start); @apply relative w-full leading-relaxed; - > .normal-text { - @apply absolute left-2 py-px pl-1 shrink-0 text-sm text-gray-400 leading-10 transition-all cursor-text; - - &.not-null { - @apply top-1 bg-white text-xs py-0 px-1 rounded-xl; - } - } - &.input-form-label { @apply py-3 pb-1; diff --git a/web/src/less/menu-btns-popup.less b/web/src/less/menu-btns-popup.less index ac4f96fc..22a48920 100644 --- a/web/src/less/menu-btns-popup.less +++ b/web/src/less/menu-btns-popup.less @@ -10,7 +10,7 @@ > .btn { .flex(row, flex-start, center); - @apply w-full py-2 px-3 text-sm rounded text-left; + @apply w-full py-2 px-3 text-base rounded text-left; > .icon { @apply block w-6 text-center mr-2 text-base; diff --git a/web/src/less/my-account-section.less b/web/src/less/my-account-section.less index 9abc9dfa..b2de21e7 100644 --- a/web/src/less/my-account-section.less +++ b/web/src/less/my-account-section.less @@ -55,12 +55,7 @@ &.password-label { > .btn { - color: @text-blue; - cursor: pointer; - - &:hover { - opacity: 0.8; - } + @apply text-blue-600 ml-1 cursor-pointer hover:opacity-80; } } } @@ -78,20 +73,7 @@ } > .reset-btn { - margin-top: 4px; - padding: 4px 8px; - background-color: @bg-red; - border: 1px solid red; - color: red; - border-radius: 4px; - line-height: 1.6; - cursor: pointer; - user-select: none; - font-size: 12px; - - &:hover { - opacity: 0.8; - } + @apply mt-2 py-1 px-2 bg-red-50 border border-red-500 text-red-600 rounded leading-4 cursor-pointer text-xs select-none hover:opacity-80; } > .usage-guide-container { @@ -99,7 +81,7 @@ @apply mt-2 w-full; > .title-text { - line-height: 2; + @apply my-2 text-sm; } > pre { diff --git a/web/src/less/setting-dialog.less b/web/src/less/setting-dialog.less index d26ed2df..d194de2c 100644 --- a/web/src/less/setting-dialog.less +++ b/web/src/less/setting-dialog.less @@ -23,7 +23,7 @@ @apply w-40 h-full shrink-0 rounded-l-lg p-4 bg-gray-100 flex flex-col justify-start items-start; > .section-item { - @apply text-sm left-6 mt-2 cursor-pointer hover:opacity-80; + @apply text-base left-6 mt-2 mb-1 cursor-pointer hover:opacity-80; &.selected { @apply font-bold hover:opacity-100; @@ -48,7 +48,7 @@ @apply w-full text-sm mb-2; > .normal-text { - @apply shrink-0; + @apply shrink-0 select-text; } } } diff --git a/web/src/services/memoService.ts b/web/src/services/memoService.ts index 89ac6917..2579fdbd 100644 --- a/web/src/services/memoService.ts +++ b/web/src/services/memoService.ts @@ -123,13 +123,13 @@ class MemoService { return memos.filter((m) => m.content.includes(memoId)); } - public async createMemo(text: string): Promise { - const memo = await api.createMemo(text); + public async createMemo(content: string): Promise { + const memo = await api.createMemo(content); return this.convertResponseModelMemo(memo); } - public async updateMemo(memoId: string, text: string): Promise { - const memo = await api.updateMemo(memoId, text); + public async updateMemo(memoId: string, content: string): Promise { + const memo = await api.updateMemo(memoId, content); return this.convertResponseModelMemo(memo); } @@ -141,6 +141,10 @@ class MemoService { await api.unpinMemo(memoId); } + public async importMemo(content: string, createdAt: string) { + await api.createMemo(content, createdAt); + } + private convertResponseModelMemo(memo: Model.Memo): Model.Memo { return { ...memo,