mirror of
https://github.com/usememos/memos.git
synced 2025-06-05 22:09:59 +02:00
feat: import data from json (#53)
This commit is contained in:
@ -16,6 +16,8 @@ type Memo struct {
|
|||||||
type MemoCreate struct {
|
type MemoCreate struct {
|
||||||
// Standard fields
|
// Standard fields
|
||||||
CreatorID int
|
CreatorID int
|
||||||
|
// Used to import memos with clearly created ts.
|
||||||
|
CreatedTs *int64 `json:"createdTs"`
|
||||||
|
|
||||||
// Domain specific fields
|
// Domain specific fields
|
||||||
Content string `json:"content"`
|
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) {
|
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(`
|
row, err := db.Db.Query(`
|
||||||
INSERT INTO memo (
|
INSERT INTO memo (
|
||||||
creator_id,
|
`+strings.Join(set, ", ")+`
|
||||||
content
|
|
||||||
)
|
)
|
||||||
VALUES (?, ?)
|
VALUES (`+strings.Join(placeholder, ",")+`)
|
||||||
RETURNING id, creator_id, created_ts, updated_ts, content, row_status
|
RETURNING id, creator_id, created_ts, updated_ts, content, row_status
|
||||||
`,
|
`,
|
||||||
create.CreatorID,
|
args...,
|
||||||
create.Content,
|
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, FormatError(err)
|
return nil, FormatError(err)
|
||||||
|
@ -35,13 +35,12 @@ const AboutSiteDialog: React.FC<Props> = ({ destroy }: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="dialog-content-container">
|
<div className="dialog-content-container">
|
||||||
<p>
|
<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>
|
||||||
<p>Built with `Golang` and `React`.</p>
|
|
||||||
<br />
|
<br />
|
||||||
<p>
|
<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">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>
|
||||||
<p className="updated-time-text">
|
<p className="updated-time-text">
|
||||||
Last updated on <span className="pre-text">{lastUpdatedAt}</span> 🎉
|
Last updated on <span className="pre-text">{lastUpdatedAt}</span> 🎉
|
||||||
|
@ -73,12 +73,10 @@ const ChangePasswordDialog: React.FC<Props> = ({ destroy }: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="dialog-content-container">
|
<div className="dialog-content-container">
|
||||||
<label className="form-label input-form-label">
|
<label className="form-label input-form-label">
|
||||||
<span className={"normal-text " + (newPassword === "" ? "" : "not-null")}>New passworld</span>
|
<input type="password" placeholder="New passworld" value={newPassword} onChange={handleNewPasswordChanged} />
|
||||||
<input type="password" value={newPassword} onChange={handleNewPasswordChanged} />
|
|
||||||
</label>
|
</label>
|
||||||
<label className="form-label input-form-label">
|
<label className="form-label input-form-label">
|
||||||
<span className={"normal-text " + (newPasswordAgain === "" ? "" : "not-null")}>Repeat the new password</span>
|
<input type="password" placeholder="Repeat the new password" value={newPasswordAgain} onChange={handleNewPasswordAgainChanged} />
|
||||||
<input type="password" value={newPasswordAgain} onChange={handleNewPasswordAgainChanged} />
|
|
||||||
</label>
|
</label>
|
||||||
<div className="btns-container">
|
<div className="btns-container">
|
||||||
<span className="btn cancel-btn" onClick={handleCloseBtnClick}>
|
<span className="btn cancel-btn" onClick={handleCloseBtnClick}>
|
||||||
|
@ -66,17 +66,13 @@ const MyAccountSection: React.FC<Props> = () => {
|
|||||||
<div className="section-container account-section-container">
|
<div className="section-container account-section-container">
|
||||||
<p className="title-text">Account Information</p>
|
<p className="title-text">Account Information</p>
|
||||||
<label className="form-label">
|
<label className="form-label">
|
||||||
<span className="normal-text">ID:</span>
|
<span className="normal-text">Email:</span>
|
||||||
<span className="normal-text">{user.id}</span>
|
<span className="normal-text">{user.email}</span>
|
||||||
</label>
|
</label>
|
||||||
<label className="form-label">
|
<label className="form-label">
|
||||||
<span className="normal-text">Created at:</span>
|
<span className="normal-text">Created at:</span>
|
||||||
<span className="normal-text">{utils.getDateString(user.createdAt)}</span>
|
<span className="normal-text">{utils.getDateString(user.createdAt)}</span>
|
||||||
</label>
|
</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">
|
<label className="form-label input-form-label username-label">
|
||||||
<span className="normal-text">Username:</span>
|
<span className="normal-text">Username:</span>
|
||||||
<input type="text" value={username} onChange={handleUsernameChanged} />
|
<input type="text" value={username} onChange={handleUsernameChanged} />
|
||||||
@ -97,12 +93,12 @@ const MyAccountSection: React.FC<Props> = () => {
|
|||||||
<label className="form-label password-label">
|
<label className="form-label password-label">
|
||||||
<span className="normal-text">Password:</span>
|
<span className="normal-text">Password:</span>
|
||||||
<span className="btn" onClick={handleChangePasswordBtnClick}>
|
<span className="btn" onClick={handleChangePasswordBtnClick}>
|
||||||
Change It
|
Change it
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div className="section-container openapi-section-container">
|
<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>
|
<p className="value-text">{openAPIRoute}</p>
|
||||||
<span className="reset-btn" onClick={handleResetOpenIdBtnClick}>
|
<span className="reset-btn" onClick={handleResetOpenIdBtnClick}>
|
||||||
Reset API
|
Reset API
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
import { useContext } from "react";
|
import { useContext } from "react";
|
||||||
import appContext from "../stores/appContext";
|
import appContext from "../stores/appContext";
|
||||||
import { globalStateService, memoService } from "../services";
|
import { globalStateService, memoService } from "../services";
|
||||||
|
import utils from "../helpers/utils";
|
||||||
import { formatMemoContent } from "./Memo";
|
import { formatMemoContent } from "./Memo";
|
||||||
|
import toastHelper from "./Toast";
|
||||||
import "../less/preferences-section.less";
|
import "../less/preferences-section.less";
|
||||||
|
|
||||||
interface Props {}
|
interface Props {}
|
||||||
@ -41,13 +43,49 @@ const PreferencesSection: React.FC<Props> = () => {
|
|||||||
const jsonStr = JSON.stringify(formatedMemos);
|
const jsonStr = JSON.stringify(formatedMemos);
|
||||||
const element = document.createElement("a");
|
const element = document.createElement("a");
|
||||||
element.setAttribute("href", "data:text/json;charset=utf-8," + encodeURIComponent(jsonStr));
|
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";
|
element.style.display = "none";
|
||||||
document.body.appendChild(element);
|
document.body.appendChild(element);
|
||||||
element.click();
|
element.click();
|
||||||
document.body.removeChild(element);
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="section-container preferences-section-container">
|
<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}>
|
<button className="px-2 py-1 border rounded text-base hover:opacity-80" onClick={handleExportBtnClick}>
|
||||||
Export data as JSON
|
Export data as JSON
|
||||||
</button>
|
</button>
|
||||||
|
<button className="ml-2 px-2 py-1 border rounded text-base hover:opacity-80" onClick={handleImportBtnClick}>
|
||||||
|
Import from JSON
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import utils from "./utils";
|
||||||
|
|
||||||
type ResponseObject<T> = {
|
type ResponseObject<T> = {
|
||||||
data: T;
|
data: T;
|
||||||
error?: string;
|
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>({
|
return request<Model.Memo>({
|
||||||
method: "POST",
|
method: "POST",
|
||||||
url: "/api/memo",
|
url: "/api/memo",
|
||||||
data: {
|
data: data,
|
||||||
content,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,14 +15,6 @@
|
|||||||
.flex(column, flex-start, flex-start);
|
.flex(column, flex-start, flex-start);
|
||||||
@apply relative w-full leading-relaxed;
|
@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 {
|
&.input-form-label {
|
||||||
@apply py-3 pb-1;
|
@apply py-3 pb-1;
|
||||||
|
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
> .btn {
|
> .btn {
|
||||||
.flex(row, flex-start, center);
|
.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 {
|
> .icon {
|
||||||
@apply block w-6 text-center mr-2 text-base;
|
@apply block w-6 text-center mr-2 text-base;
|
||||||
|
@ -55,12 +55,7 @@
|
|||||||
|
|
||||||
&.password-label {
|
&.password-label {
|
||||||
> .btn {
|
> .btn {
|
||||||
color: @text-blue;
|
@apply text-blue-600 ml-1 cursor-pointer hover:opacity-80;
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
opacity: 0.8;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -78,20 +73,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
> .reset-btn {
|
> .reset-btn {
|
||||||
margin-top: 4px;
|
@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;
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
> .usage-guide-container {
|
> .usage-guide-container {
|
||||||
@ -99,7 +81,7 @@
|
|||||||
@apply mt-2 w-full;
|
@apply mt-2 w-full;
|
||||||
|
|
||||||
> .title-text {
|
> .title-text {
|
||||||
line-height: 2;
|
@apply my-2 text-sm;
|
||||||
}
|
}
|
||||||
|
|
||||||
> pre {
|
> 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;
|
@apply w-40 h-full shrink-0 rounded-l-lg p-4 bg-gray-100 flex flex-col justify-start items-start;
|
||||||
|
|
||||||
> .section-item {
|
> .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 {
|
&.selected {
|
||||||
@apply font-bold hover:opacity-100;
|
@apply font-bold hover:opacity-100;
|
||||||
@ -48,7 +48,7 @@
|
|||||||
@apply w-full text-sm mb-2;
|
@apply w-full text-sm mb-2;
|
||||||
|
|
||||||
> .normal-text {
|
> .normal-text {
|
||||||
@apply shrink-0;
|
@apply shrink-0 select-text;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -123,13 +123,13 @@ class MemoService {
|
|||||||
return memos.filter((m) => m.content.includes(memoId));
|
return memos.filter((m) => m.content.includes(memoId));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async createMemo(text: string): Promise<Model.Memo> {
|
public async createMemo(content: string): Promise<Model.Memo> {
|
||||||
const memo = await api.createMemo(text);
|
const memo = await api.createMemo(content);
|
||||||
return this.convertResponseModelMemo(memo);
|
return this.convertResponseModelMemo(memo);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async updateMemo(memoId: string, text: string): Promise<Model.Memo> {
|
public async updateMemo(memoId: string, content: string): Promise<Model.Memo> {
|
||||||
const memo = await api.updateMemo(memoId, text);
|
const memo = await api.updateMemo(memoId, content);
|
||||||
return this.convertResponseModelMemo(memo);
|
return this.convertResponseModelMemo(memo);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -141,6 +141,10 @@ class MemoService {
|
|||||||
await api.unpinMemo(memoId);
|
await api.unpinMemo(memoId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async importMemo(content: string, createdAt: string) {
|
||||||
|
await api.createMemo(content, createdAt);
|
||||||
|
}
|
||||||
|
|
||||||
private convertResponseModelMemo(memo: Model.Memo): Model.Memo {
|
private convertResponseModelMemo(memo: Model.Memo): Model.Memo {
|
||||||
return {
|
return {
|
||||||
...memo,
|
...memo,
|
||||||
|
Reference in New Issue
Block a user