mirror of
https://github.com/usememos/memos.git
synced 2024-12-19 00:51:30 +03:00
feat: import data from json (#53)
This commit is contained in:
parent
af4b61143f
commit
615cec3066
@ -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"`
|
||||
|
@ -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)
|
||||
|
@ -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> 🎉
|
||||
|
@ -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}>
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
</>
|
||||
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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 {
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
Loading…
Reference in New Issue
Block a user