chore: upgrade version 0.13.1 (#1754)

This commit is contained in:
boojack 2023-05-27 09:09:41 +08:00 committed by GitHub
parent 93d608f050
commit 2e34ce90a1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 48 additions and 303 deletions

View File

@ -1,66 +0,0 @@
name: "E2E Test"
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
build:
name: Build and Run Memos With E2E Test
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Build Docker image
id: docker_build
uses: docker/build-push-action@v3
with:
context: ./
file: ./Dockerfile
platforms: linux/amd64
push: false
tags: neosmemo/memos:e2e
labels: neosmemo/memos:e2e
- name: Run Docker container
run: docker run -d -p 5230:5230 neosmemo/memos:e2e
- uses: pnpm/action-setup@v2.2.4
with:
version: 8.0.0
- uses: actions/setup-node@v3
with:
node-version: 18
cache: "pnpm"
cache-dependency-path: ./web/pnpm-lock.yaml
- name: Install dependencies
working-directory: web
run: pnpm install
- name: Install Playwright Browsers
working-directory: web
run: npx playwright install --with-deps
- name: Run Playwright tests
working-directory: web
run: npx playwright test
- uses: actions/upload-artifact@v3
if: always()
with:
name: playwright-report
path: web/playwright-report/
retention-days: 30
- uses: actions/upload-artifact@v3
if: always()
with:
name: playwright-screenshot
path: web/playwright-screenshot/
retention-days: 30
- name: Stop Docker container
run: docker stop $(docker ps -q)

View File

@ -123,6 +123,7 @@ func (s *Server) registerResourceRoutes(g *echo.Group) {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to unmarshal storage service id").SetInternal(err) return echo.NewHTTPError(http.StatusInternalServerError, "Failed to unmarshal storage service id").SetInternal(err)
} }
} }
publicID := common.GenUUID() publicID := common.GenUUID()
if storageServiceID == api.DatabaseStorage { if storageServiceID == api.DatabaseStorage {
fileBytes, err := io.ReadAll(sourceFile) fileBytes, err := io.ReadAll(sourceFile)
@ -326,11 +327,12 @@ func (s *Server) registerResourceRoutes(g *echo.Group) {
if err := os.Remove(resource.InternalPath); err != nil { if err := os.Remove(resource.InternalPath); err != nil {
log.Warn(fmt.Sprintf("failed to delete local file with path %s", resource.InternalPath), zap.Error(err)) log.Warn(fmt.Sprintf("failed to delete local file with path %s", resource.InternalPath), zap.Error(err))
} }
}
thumbnailPath := path.Join(s.Profile.Data, thumbnailImagePath, resource.PublicID) ext := filepath.Ext(resource.Filename)
if err := os.Remove(thumbnailPath); err != nil { thumbnailPath := path.Join(s.Profile.Data, thumbnailImagePath, fmt.Sprintf("%d-%s%s", resource.ID, resource.PublicID, ext))
log.Warn(fmt.Sprintf("failed to delete local thumbnail with path %s", thumbnailPath), zap.Error(err)) if err := os.Remove(thumbnailPath); err != nil {
} log.Warn(fmt.Sprintf("failed to delete local thumbnail with path %s", thumbnailPath), zap.Error(err))
} }
resourceDelete := &api.ResourceDelete{ resourceDelete := &api.ResourceDelete{
@ -434,7 +436,7 @@ func (s *Server) registerResourcePublicRoutes(g *echo.Group) {
if c.QueryParam("thumbnail") == "1" && common.HasPrefixes(resource.Type, "image/png", "image/jpeg") { if c.QueryParam("thumbnail") == "1" && common.HasPrefixes(resource.Type, "image/png", "image/jpeg") {
ext := filepath.Ext(filename) ext := filepath.Ext(filename)
thumbnailPath := path.Join(s.Profile.Data, thumbnailImagePath, resource.PublicID+ext) thumbnailPath := path.Join(s.Profile.Data, thumbnailImagePath, fmt.Sprintf("%d-%s%s", resource.ID, resource.PublicID, ext))
thumbnailBlob, err := getOrGenerateThumbnailImage(blob, thumbnailPath) thumbnailBlob, err := getOrGenerateThumbnailImage(blob, thumbnailPath)
if err != nil { if err != nil {
log.Warn(fmt.Sprintf("failed to get or generate local thumbnail with path %s", thumbnailPath), zap.Error(err)) log.Warn(fmt.Sprintf("failed to get or generate local thumbnail with path %s", thumbnailPath), zap.Error(err))

View File

@ -9,10 +9,10 @@ import (
// Version is the service current released version. // Version is the service current released version.
// Semantic versioning: https://semver.org/ // Semantic versioning: https://semver.org/
var Version = "0.13.0" var Version = "0.13.1"
// DevVersion is the service current development version. // DevVersion is the service current development version.
var DevVersion = "0.13.0" var DevVersion = "0.13.1"
func GetCurrentVersion(mode string) string { func GetCurrentVersion(mode string) string {
if mode == "dev" || mode == "demo" { if mode == "dev" || mode == "demo" {

View File

@ -3,7 +3,6 @@ ALTER TABLE
ADD ADD
COLUMN public_id TEXT NOT NULL DEFAULT ''; COLUMN public_id TEXT NOT NULL DEFAULT '';
-- TODO(steven): remove this in next release.
CREATE UNIQUE INDEX resource_id_public_id_unique_index ON resource (id, public_id); CREATE UNIQUE INDEX resource_id_public_id_unique_index ON resource (id, public_id);
UPDATE UPDATE

View File

@ -6,6 +6,7 @@ import (
"fmt" "fmt"
"strconv" "strconv"
"strings" "strings"
"time"
"github.com/usememos/memos/common" "github.com/usememos/memos/common"
) )
@ -90,6 +91,10 @@ func (s *Store) CreateMemo(ctx context.Context, create *MemoMessage) (*MemoMessa
} }
defer tx.Rollback() defer tx.Rollback()
if create.CreatedTs == 0 {
create.CreatedTs = time.Now().Unix()
}
query := ` query := `
INSERT INTO memo ( INSERT INTO memo (
creator_id, creator_id,

View File

@ -1,13 +0,0 @@
import { test } from "@playwright/test";
import { signUp } from "./utils";
test.use({
locale: "en-US",
timezoneId: "Europe/Berlin",
});
test.describe("Sign up a host account", async () => {
test("Sign Up", async ({ page }) => {
await signUp(page, "admin", "admin");
});
});

View File

@ -1,37 +0,0 @@
import { test, expect } from "@playwright/test";
import { review, login, writeMemo } from "./utils";
import randomstring from "randomstring";
test.use({
locale: "en-US",
timezoneId: "Europe/Berlin",
});
test.beforeEach(async ({ page }) => {
await login(page, "admin", "admin");
});
test.describe("Write some memos", async () => {
test("Write memos", async ({ page }) => {
const content = `${randomstring.generate()} from Write memos`;
await writeMemo(page, content);
await expect(page.getByText(content)).toBeVisible();
});
test("Write memos with Tag", async ({ page }) => {
const tag = randomstring.generate(5);
const content = `#${tag} ${randomstring.generate()} from Write memos with Tag`;
await writeMemo(page, content);
// 1.memo contentg 2.tags list of memos editor 3.tags list
await expect(page.getByText(tag)).toHaveCount(3);
});
});
test.describe("Daily Review", async () => {
test("Daily Review", async ({ page }) => {
const content = randomstring.generate();
await writeMemo(page, content);
await review(page);
await expect(page.getByText(content)).toBeVisible();
});
});

View File

@ -1,2 +0,0 @@
const baseHost = "http://localhost:5230";
export { baseHost };

View File

@ -1,52 +0,0 @@
import { expect, Page } from "@playwright/test";
import locale from "../src/locales/en.json";
import { baseHost } from "./fixtures";
async function screenshot(page: Page, name: string) {
await page.screenshot({ path: `playwright-screenshot/${name}.png`, fullPage: true });
}
async function writeMemo(page: Page, content: string) {
await expect(page.getByRole("button", { name: locale.editor.save })).toBeDisabled();
await page.getByPlaceholder("Any thoughts...").fill(content);
await expect(page.getByRole("button", { name: locale.editor.save })).toBeEnabled();
await page.getByRole("button", { name: locale.editor.save }).click();
}
async function login(page: Page, username: string, password: string) {
page.goto(`${baseHost}/`);
await screenshot(page, "explore-page");
await page.waitForURL("**/explore");
await screenshot(page, "explore-page-after-wait");
await page.getByRole("link", { name: locale.common["sign-in"] }).click();
await screenshot(page, "auth-page");
await page.waitForURL("**/auth");
await page.locator('input[type="text"]').click();
await page.locator('input[type="text"]').fill(username);
await page.locator('input[type="password"]').click();
await page.locator('input[type="password"]').fill(password);
await page.getByRole("button", { name: locale.common["sign-in"] }).click();
await page.waitForTimeout(1000);
await screenshot(page, "home-page-login-success");
}
async function signUp(page: Page, username: string, password: string) {
await page.goto(`${baseHost}/`);
await page.waitForURL("**/auth");
await screenshot(page, "sign-up-page");
await page.locator('input[type="text"]').click();
await page.locator('input[type="text"]').fill(username);
await page.locator('input[type="password"]').click();
await page.locator('input[type="password"]').fill(password);
await page.getByRole("button", { name: locale.auth["signup-as-host"] }).click();
await page.waitForTimeout(1000);
await screenshot(page, "home-page-sign-up-success");
}
async function review(page: Page) {
await page.goto(`${baseHost}/`);
await page.getByRole("link", { name: locale["daily-review"]["title"] }).click();
await screenshot(page, "review");
}
export { writeMemo, login, signUp, review };

View File

@ -35,7 +35,6 @@
"zustand": "^4.3.6" "zustand": "^4.3.6"
}, },
"devDependencies": { "devDependencies": {
"@playwright/test": "^1.32.2",
"@types/lodash-es": "^4.17.5", "@types/lodash-es": "^4.17.5",
"@types/node": "^18.0.3", "@types/node": "^18.0.3",
"@types/qs": "^6.9.7", "@types/qs": "^6.9.7",

View File

@ -1,19 +0,0 @@
import { defineConfig, devices } from "@playwright/test";
export default defineConfig({
testDir: "./e2e-tests",
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: 1,
reporter: [["html", { outputFolder: "playwright-report", open: "never" }]],
use: {
trace: "on-first-retry",
},
projects: [
{
name: "chromium",
use: { ...devices["Desktop Chrome"] },
},
],
});

20
web/pnpm-lock.yaml generated
View File

@ -78,9 +78,6 @@ dependencies:
version: 4.3.6(react@18.2.0) version: 4.3.6(react@18.2.0)
devDependencies: devDependencies:
'@playwright/test':
specifier: ^1.32.2
version: 1.32.2
'@types/lodash-es': '@types/lodash-es':
specifier: ^4.17.5 specifier: ^4.17.5
version: 4.17.5 version: 4.17.5
@ -784,17 +781,6 @@ packages:
'@nodelib/fs.scandir': 2.1.5 '@nodelib/fs.scandir': 2.1.5
fastq: 1.15.0 fastq: 1.15.0
/@playwright/test@1.32.2:
resolution: {integrity: sha512-nhaTSDpEdTTttdkDE8Z6K3icuG1DVRxrl98Qq0Lfc63SS9a2sjc9+x8ezysh7MzCKz6Y+nArml3/mmt+gqRmQQ==}
engines: {node: '>=14'}
hasBin: true
dependencies:
'@types/node': 18.0.3
playwright-core: 1.32.2
optionalDependencies:
fsevents: 2.3.2
dev: true
/@popperjs/core@2.11.7: /@popperjs/core@2.11.7:
resolution: {integrity: sha512-Cr4OjIkipTtcXKjAsm8agyleBuDHvxzeBoa1v543lbv1YaIwQjESsVcmjiWiPEbC1FIeHOG/Op9kdCmAmiS3Kw==} resolution: {integrity: sha512-Cr4OjIkipTtcXKjAsm8agyleBuDHvxzeBoa1v543lbv1YaIwQjESsVcmjiWiPEbC1FIeHOG/Op9kdCmAmiS3Kw==}
dev: false dev: false
@ -2944,12 +2930,6 @@ packages:
dev: true dev: true
optional: true optional: true
/playwright-core@1.32.2:
resolution: {integrity: sha512-zD7aonO+07kOTthsrCR3YCVnDcqSHIJpdFUtZEMOb6//1Rc7/6mZDRdw+nlzcQiQltOOsiqI3rrSyn/SlyjnJQ==}
engines: {node: '>=14'}
hasBin: true
dev: true
/postcss-import@14.1.0(postcss@8.4.21): /postcss-import@14.1.0(postcss@8.4.21):
resolution: {integrity: sha512-flwI+Vgm4SElObFVPpTIT7SU7R3qk2L7PyduMcokiaVKuWv9d/U+Gm/QAd8NDLuykTWTkcrjOeD2Pp1rMeBTGw==} resolution: {integrity: sha512-flwI+Vgm4SElObFVPpTIT7SU7R3qk2L7PyduMcokiaVKuWv9d/U+Gm/QAd8NDLuykTWTkcrjOeD2Pp1rMeBTGw==}
engines: {node: '>=10.0.0'} engines: {node: '>=10.0.0'}

View File

@ -1,17 +1,4 @@
import { import { Button, FormControl, Input, Modal, ModalClose, ModalDialog, Stack, Textarea, Typography } from "@mui/joy";
Button,
FormControl,
FormLabel,
Input,
Menu,
MenuItem,
Modal,
ModalClose,
ModalDialog,
Stack,
Textarea,
Typography,
} from "@mui/joy";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { toast } from "react-hot-toast"; import { toast } from "react-hot-toast";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@ -19,11 +6,11 @@ import * as api from "@/helpers/api";
import useLoading from "@/hooks/useLoading"; import useLoading from "@/hooks/useLoading";
import { marked } from "@/labs/marked"; import { marked } from "@/labs/marked";
import { useMessageStore } from "@/store/zustand/message"; import { useMessageStore } from "@/store/zustand/message";
import { defaultMessageGroup, MessageGroup, useMessageGroupStore } from "@/store/zustand/message-group";
import Icon from "./Icon"; import Icon from "./Icon";
import { generateDialog } from "./Dialog"; import { generateDialog } from "./Dialog";
import showSettingDialog from "./SettingDialog"; import showSettingDialog from "./SettingDialog";
import { defaultMessageGroup, MessageGroup, useMessageGroupStore } from "@/store/zustand/message-group"; import Selector from "./kit/Selector";
import { PlusIcon, Trash2Icon } from "lucide-react";
type Props = DialogProps; type Props = DialogProps;
@ -92,26 +79,21 @@ const AskAIDialog: React.FC<Props> = (props: Props) => {
}); });
}; };
const [anchorEl, setAnchorEl] = useState<null | (EventTarget & Element)>(null); const handleMessageGroupSelect = (value: string) => {
const handleMenuOpen = (event: React.SyntheticEvent) => { const messageGroup = messageGroupList.find((group) => group.messageStorageId === value);
setAnchorEl(event.currentTarget); if (messageGroup) {
}; setMessageGroup(messageGroup);
const handleMenuClose = () => { }
setAnchorEl(null);
};
const handleOptionSelect = (option: MessageGroup) => {
setMessageGroup(option);
setAnchorEl(null);
}; };
const [isAddMessageGroupDlgOpen, setIsAddMessageGroupDlgOpen] = useState<boolean>(false); const [isAddMessageGroupDialogOpen, setIsAddMessageGroupDialogOpen] = useState<boolean>(false);
const [groupName, setGroupName] = useState<string>(""); const [groupName, setGroupName] = useState<string>("");
const messageGroupStore = useMessageGroupStore(); const messageGroupStore = useMessageGroupStore();
const messageGroupList = messageGroupStore.groupList; const messageGroupList = messageGroupStore.groupList;
const handleOpenDialog = () => { const handleOpenDialog = () => {
setIsAddMessageGroupDlgOpen(true); setIsAddMessageGroupDialogOpen(true);
}; };
const handleRemoveDialog = () => { const handleRemoveDialog = () => {
@ -119,7 +101,7 @@ const AskAIDialog: React.FC<Props> = (props: Props) => {
}; };
const handleCloseDialog = () => { const handleCloseDialog = () => {
setIsAddMessageGroupDlgOpen(false); setIsAddMessageGroupDialogOpen(false);
setGroupName(""); setGroupName("");
}; };
@ -142,29 +124,24 @@ const AskAIDialog: React.FC<Props> = (props: Props) => {
<div className="dialog-header-container"> <div className="dialog-header-container">
<p className="title-text flex flex-row items-center"> <p className="title-text flex flex-row items-center">
<Icon.Bot className="mr-1 w-5 h-auto opacity-80" /> <Icon.Bot className="mr-1 w-5 h-auto opacity-80" />
{t("ask-ai.title")} <span className="mr-4">{t("ask-ai.title")}</span>
<span className="button-group" style={{ marginLeft: "10px" }}> <span className="flex flex-row justify-start items-center">
<Button color={"primary"} onClick={handleMenuOpen}> <Selector
<div className="button-len-max-150">{messageGroup.name}</div> className="w-32"
</Button> dataSource={messageGroupList.map((item) => ({ text: item.name, value: item.messageStorageId }))}
<Button color={"success"} onClick={handleOpenDialog}> value={messageGroup.messageStorageId}
<PlusIcon size={"13px"} /> handleValueChanged={handleMessageGroupSelect}
</Button> />
<Button color={"danger"} onClick={handleRemoveDialog}> <button className="btn-text px-1 ml-1" onClick={handleOpenDialog}>
<Trash2Icon size={"13px"} /> <Icon.Plus className="w-4 h-auto" />
</Button> </button>
<button className="btn-text px-1" onClick={handleRemoveDialog}>
<Icon.Trash2 className="w-4 h-auto" />
</button>
</span> </span>
</p> </p>
<Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleMenuClose}> <Modal open={isAddMessageGroupDialogOpen} onClose={handleCloseDialog}>
<MenuItem onClick={() => handleOptionSelect(defaultMessageGroup)}>{defaultMessageGroup.name}</MenuItem>
{messageGroupList.map((messageGroup, index) => (
<MenuItem key={index} onClick={() => handleOptionSelect(messageGroup)}>
{messageGroup.name}
</MenuItem>
))}
</Menu>
<Modal open={isAddMessageGroupDlgOpen} onClose={handleCloseDialog}>
<ModalDialog aria-labelledby="basic-modal-dialog-title" sx={{ maxWidth: 500 }}> <ModalDialog aria-labelledby="basic-modal-dialog-title" sx={{ maxWidth: 500 }}>
<ModalClose /> <ModalClose />
<Typography id="basic-modal-dialog-title" component="h2"> <Typography id="basic-modal-dialog-title" component="h2">
@ -172,19 +149,18 @@ const AskAIDialog: React.FC<Props> = (props: Props) => {
</Typography> </Typography>
<Stack spacing={2}> <Stack spacing={2}>
<FormControl> <FormControl>
<FormLabel>{t("ask-ai.label-message-group-name-title")}</FormLabel>
<Input <Input
value={groupName} value={groupName}
onChange={(e) => setGroupName(e.target.value)} onChange={(e) => setGroupName(e.target.value)}
placeholder={t("ask-ai.label-message-group-name-title")} placeholder={t("ask-ai.label-message-group-name-title")}
/> />
</FormControl> </FormControl>
<Typography> <div className="w-full flex justify-end gap-x-2">
<Button onClick={handleCancel} style={{ marginRight: "10px" }}> <Button variant="plain" onClick={handleCancel}>
{t("common.cancel")} {t("common.cancel")}
</Button> </Button>
<Button onClick={handleAddMessageGroupDlgConfirm}>{t("common.confirm")}</Button> <Button onClick={handleAddMessageGroupDlgConfirm}>{t("common.confirm")}</Button>
</Typography> </div>
</Stack> </Stack>
</ModalDialog> </ModalDialog>
</Modal> </Modal>

View File

@ -282,7 +282,7 @@ const Memo: React.FC<Props> = (props: Props) => {
{showRelatedMemos && relatedMemoList.length > 0 && ( {showRelatedMemos && relatedMemoList.length > 0 && (
<> <>
<p className="text-sm mt-4 mb-1 pl-4 opacity-50 flex flex-row items-center"> <p className="text-sm dark:text-gray-300 mt-4 mb-1 pl-4 opacity-50 flex flex-row items-center">
<Icon.Link className="w-4 h-auto mr-1" /> <Icon.Link className="w-4 h-auto mr-1" />
<span>Related memos</span> <span>Related memos</span>
</p> </p>

View File

@ -28,7 +28,7 @@ const MobileHeader = (props: Props) => {
}, [filter, shortcuts]); }, [filter, shortcuts]);
return ( return (
<div className="sticky top-0 pt-4 pb-1 mb-1 backdrop-blur-sm flex md:hidden flex-row justify-between items-center w-full h-auto flex-nowrap shrink-0 z-2"> <div className="sticky top-0 pt-4 pb-1 mb-1 backdrop-blur bg-zinc-100 dark:bg-zinc-800 bg-opacity-70 flex md:hidden flex-row justify-between items-center w-full h-auto flex-nowrap shrink-0 z-2">
<div className="flex flex-row justify-start items-center mr-2 shrink-0 overflow-hidden"> <div className="flex flex-row justify-start items-center mr-2 shrink-0 overflow-hidden">
<div <div
className="flex sm:hidden flex-row justify-center items-center w-6 h-6 mr-1 shrink-0 bg-transparent" className="flex sm:hidden flex-row justify-center items-center w-6 h-6 mr-1 shrink-0 bg-transparent"

View File

@ -1,38 +1,11 @@
html, html,
body { body {
@apply text-base w-full h-full dark:bg-zinc-800; @apply text-base w-full h-full dark:bg-zinc-800;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "PingFang SC", "Noto Sans", "Noto Sans CJK SC", font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "PingFang SC", "Noto Sans",
"Microsoft YaHei UI", "Microsoft YaHei", "WenQuanYi Micro Hei", "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji", "Noto Sans CJK SC", "Microsoft YaHei UI", "Microsoft YaHei", "WenQuanYi Micro Hei", "Apple Color Emoji", "Segoe UI Emoji",
sans-serif; "Segoe UI Symbol", "Noto Color Emoji", sans-serif;
} }
#root { #root {
@apply w-full h-full; @apply w-full h-full;
} }
.button-group {
display: flex;
gap: 0; /* 按钮之间的间距 */
}
.button-group>button:not(:first-child):not(:last-child) {
border-radius: 0;
}
.button-group>button:first-child {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
.button-group>button:last-child {
border-bottom-left-radius: 0;
border-top-left-radius: 0;
}
.button-len-max-150 {
max-width: 150px; /* 按钮的最大宽度 */
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}