feat: import data from json (#53)

This commit is contained in:
STEVEN 2022-05-15 22:21:13 +08:00 committed by GitHub
parent af4b61143f
commit 615cec3066
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 90 additions and 61 deletions

View File

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

View File

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

View File

@ -35,13 +35,12 @@ const AboutSiteDialog: React.FC<Props> = ({ destroy }: Props) => {
</div>
<div className="dialog-content-container">
<p>
Memos is an open source, self-hosted alternative to <a href="https://flomoapp.com">flomo</a>.
Memos is an open source, quickly self-hosted alternative <a href="https://flomoapp.com">flomo</a>.
</p>
<p>Built with `Golang` and `React`.</p>
<br />
<p>
🏗 <a href="https://github.com/justmemos/memos">This project</a> is working in progress, and very pleasure to your{" "}
<a href="https://github.com/justmemos/memos/issues">issues</a>.
<a href="https://github.com/justmemos/memos/issues">PRs</a>.
</p>
<p className="updated-time-text">
Last updated on <span className="pre-text">{lastUpdatedAt}</span> 🎉

View File

@ -73,12 +73,10 @@ const ChangePasswordDialog: React.FC<Props> = ({ destroy }: Props) => {
</div>
<div className="dialog-content-container">
<label className="form-label input-form-label">
<span className={"normal-text " + (newPassword === "" ? "" : "not-null")}>New passworld</span>
<input type="password" value={newPassword} onChange={handleNewPasswordChanged} />
<input type="password" placeholder="New passworld" value={newPassword} onChange={handleNewPasswordChanged} />
</label>
<label className="form-label input-form-label">
<span className={"normal-text " + (newPasswordAgain === "" ? "" : "not-null")}>Repeat the new password</span>
<input type="password" value={newPasswordAgain} onChange={handleNewPasswordAgainChanged} />
<input type="password" placeholder="Repeat the new password" value={newPasswordAgain} onChange={handleNewPasswordAgainChanged} />
</label>
<div className="btns-container">
<span className="btn cancel-btn" onClick={handleCloseBtnClick}>

View File

@ -66,17 +66,13 @@ const MyAccountSection: React.FC<Props> = () => {
<div className="section-container account-section-container">
<p className="title-text">Account Information</p>
<label className="form-label">
<span className="normal-text">ID:</span>
<span className="normal-text">{user.id}</span>
<span className="normal-text">Email:</span>
<span className="normal-text">{user.email}</span>
</label>
<label className="form-label">
<span className="normal-text">Created at:</span>
<span className="normal-text">{utils.getDateString(user.createdAt)}</span>
</label>
<label className="form-label">
<span className="normal-text">Email:</span>
<span className="normal-text">{user.email}</span>
</label>
<label className="form-label input-form-label username-label">
<span className="normal-text">Username:</span>
<input type="text" value={username} onChange={handleUsernameChanged} />
@ -97,12 +93,12 @@ const MyAccountSection: React.FC<Props> = () => {
<label className="form-label password-label">
<span className="normal-text">Password:</span>
<span className="btn" onClick={handleChangePasswordBtnClick}>
Change It
Change it
</span>
</label>
</div>
<div className="section-container openapi-section-container">
<p className="title-text">Open API (Experimental feature)</p>
<p className="title-text">Open API</p>
<p className="value-text">{openAPIRoute}</p>
<span className="reset-btn" onClick={handleResetOpenIdBtnClick}>
Reset API

View File

@ -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<Props> = () => {
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 (
<>
<div className="section-container preferences-section-container">
@ -75,6 +113,9 @@ const PreferencesSection: React.FC<Props> = () => {
<button className="px-2 py-1 border rounded text-base hover:opacity-80" onClick={handleExportBtnClick}>
Export data as JSON
</button>
<button className="ml-2 px-2 py-1 border rounded text-base hover:opacity-80" onClick={handleImportBtnClick}>
Import from JSON
</button>
</div>
</div>
</>

View File

@ -1,3 +1,5 @@
import utils from "./utils";
type ResponseObject<T> = {
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<Model.Memo>({
method: "POST",
url: "/api/memo",
data: {
content,
},
data: data,
});
}

View File

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

View File

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

View File

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

View File

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

View File

@ -123,13 +123,13 @@ class MemoService {
return memos.filter((m) => m.content.includes(memoId));
}
public async createMemo(text: string): Promise<Model.Memo> {
const memo = await api.createMemo(text);
public async createMemo(content: string): Promise<Model.Memo> {
const memo = await api.createMemo(content);
return this.convertResponseModelMemo(memo);
}
public async updateMemo(memoId: string, text: string): Promise<Model.Memo> {
const memo = await api.updateMemo(memoId, text);
public async updateMemo(memoId: string, content: string): Promise<Model.Memo> {
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,