pull/52/head
sunface 4 years ago
parent fe2090db57
commit 39b01f4cca

@ -17,7 +17,8 @@ import {
TagLabel, TagLabel,
TagCloseButton, TagCloseButton,
Spinner, Spinner,
Text Text,
Select
} from "@chakra-ui/react" } from "@chakra-ui/react"
import { useViewportScroll } from "framer-motion" import { useViewportScroll } from "framer-motion"
import NextLink from "next/link" import NextLink from "next/link"
@ -29,6 +30,7 @@ import EditModeSelect from "components/edit-mode-select"
import Tags from "components/tags/tags" import Tags from "components/tags/tags"
import { Story } from "src/types/story" import { Story } from "src/types/story"
import { FaCloud } from "react-icons/fa" import { FaCloud } from "react-icons/fa"
import { requestApi } from "utils/axios/request"
@ -44,10 +46,21 @@ interface Props {
function HeaderContent(props: Props) { function HeaderContent(props: Props) {
const { isOpen, onOpen, onClose } = useDisclosure() const { isOpen, onOpen, onClose } = useDisclosure()
const [orgs,setOrgs] = useState([{id:"", nickname:"Personal"}])
const onTagsChange = ids => { const onTagsChange = ids => {
props.ar.tags = ids props.ar.tags = ids
} }
useEffect(() => {
getOrgs()
},[])
const getOrgs = async() => {
const res = await requestApi.get(`/org/byUserID/0`)
res.data.unshift({id:"", nickname:"Personal"})
setOrgs(res.data)
}
return ( return (
<> <>
@ -95,6 +108,17 @@ function HeaderContent(props: Props) {
<Divider mt="5" mb="5"/> <Divider mt="5" mb="5"/>
<Card> <Card>
<Heading size="xs">
</Heading>
<Select value={props.ar.ownerId??""} onChange={(e) => {props.ar.ownerId = e.target.value; props.onChange()}} variant="unstyled" mt="3">
{
orgs.map(o => <option value={o.id}>{o.nickname}</option>)
}
</Select>
</Card>
<Card mt="4">
<Heading size="xs"> <Heading size="xs">
</Heading> </Heading>

@ -1,4 +1,4 @@
import { Text, Box, Heading, Image, HStack, Center, Button, Flex, FormControl, FormLabel, Input, FormErrorMessage, VStack, Textarea, Divider, useToast, Stack, StackDivider, useColorModeValue, Table, Thead, Tr, Th, Tbody, Td, CloseButton, Editable, EditablePreview, EditableInput } from "@chakra-ui/react" import { Text, Box, Heading, Image, HStack, Center, Button, Flex, FormControl, FormLabel, Input, FormErrorMessage, VStack, Textarea, Divider, useToast, Stack, StackDivider, useColorModeValue, Table, Thead, Tr, Th, Tbody, Td, CloseButton, Editable, EditablePreview, EditableInput, Select } from "@chakra-ui/react"
import Card from "components/card" import Card from "components/card"
import Sidebar from "layouts/sidebar/sidebar" import Sidebar from "layouts/sidebar/sidebar"
import React, { useEffect, useState } from "react" import React, { useEffect, useState } from "react"
@ -24,7 +24,7 @@ var validator = require('validator');
const newSeries: Story = { title: '', brief: '', cover: '', type: IDType.Series } const newSeries: Story = { title: '', brief: '', cover: '', type: IDType.Series }
const PostsPage = () => { const PostsPage = () => {
const [currentSeries, setCurrentSeries]:[Story,any] = useState(null) const [currentSeries, setCurrentSeries]: [Story, any] = useState(null)
const [series, setSeries] = useState([]) const [series, setSeries] = useState([])
const [posts, setPosts] = useState([]) const [posts, setPosts] = useState([])
const [seriesPosts, setSeriesPosts] = useState([]) const [seriesPosts, setSeriesPosts] = useState([])
@ -80,7 +80,7 @@ const PostsPage = () => {
// 这里必须按照顺序同步提交 // 这里必须按照顺序同步提交
await requestApi.post(`/story`, values) await requestApi.post(`/story`, values)
await requestApi.post(`/story/series/post/${values.id}`, seriesPosts) await requestApi.post(`/story/series/post/${values.id}`, seriesPosts)
toast({ toast({
description: "提交成功", description: "提交成功",
@ -124,17 +124,17 @@ const PostsPage = () => {
setSeriesPosts(sposts) setSeriesPosts(sposts)
} }
const onPriorityChange = (e,s) => { const onPriorityChange = (e, s) => {
if (e.currentTarget.value) { if (e.currentTarget.value) {
const i = parseInt(e.currentTarget.value) const i = parseInt(e.currentTarget.value)
if (i) { if (i) {
s.priority = i s.priority = i
} }
} else { } else {
s.priority = 0 s.priority = 0
} }
const sposts = cloneDeep(seriesPosts) const sposts = cloneDeep(seriesPosts)
setSeriesPosts(sposts) setSeriesPosts(sposts)
} }
@ -174,6 +174,19 @@ const PostsPage = () => {
</FormControl> </FormControl>
)} )}
</Field> </Field>
{/* <Field name="ownerId">
{({ field, form }) => (
<FormControl isInvalid={form.errors.ownerId && form.touched.ownerId} >
<FormLabel></FormLabel>
<Select {...field} variant="unstyled" mt="3">
{
orgs.map(o => <option value={o.id}>{o.nickname}</option>)
}
</Select>
<FormErrorMessage>{form.errors.ownerId}</FormErrorMessage>
</FormControl>
)}
</Field> */}
<Field name="cover" validate={validateUrl}> <Field name="cover" validate={validateUrl}>
{({ field, form }) => ( {({ field, form }) => (
<FormControl isInvalid={form.errors.cover && form.touched.cover}> <FormControl isInvalid={form.errors.cover && form.touched.cover}>
@ -196,7 +209,7 @@ const PostsPage = () => {
{({ field, form }) => ( {({ field, form }) => (
<FormControl> <FormControl>
<FormLabel></FormLabel> <FormLabel></FormLabel>
<Tags tags={currentSeries.tags} onChange={(ids) => currentSeries.tags = ids}/> <Tags tags={currentSeries.tags} onChange={(ids) => currentSeries.tags = ids} />
</FormControl> </FormControl>
)} )}
</Field> </Field>
@ -229,8 +242,8 @@ const PostsPage = () => {
<Td>{post.title}</Td> <Td>{post.title}</Td>
<Td> <Td>
<Editable value={s.priority}> <Editable value={s.priority}>
<EditablePreview minWidth="100px"/> <EditablePreview minWidth="100px" />
<EditableInput onChange={(e) => onPriorityChange(e,s)}/> <EditableInput onChange={(e) => onPriorityChange(e, s)} />
</Editable> </Editable>
</Td> </Td>
<Td width="50px"><CloseButton size="sm" onClick={() => onPostDelete(s.id)} _focus={null} /></Td> <Td width="50px"><CloseButton size="sm" onClick={() => onPostDelete(s.id)} _focus={null} /></Td>
@ -277,7 +290,7 @@ const PostsPage = () => {
<VStack mt="4"> <VStack mt="4">
{series.map(post => {series.map(post =>
<Box width="100%" key={post.id}> <Box width="100%" key={post.id}>
<TextStoryCard story={post} showActions={true} mt="4" onEdit={() => editSeries(post)} onDelete={() => onDeleteSeries(post.id)} showSource={false} onPin={() => onPinPost(post.id)}/> <TextStoryCard story={post} showActions={true} mt="4" onEdit={() => editSeries(post)} onDelete={() => onDeleteSeries(post.id)} showSource={false} onPin={() => onPinPost(post.id)} />
<Divider mt="5" /> <Divider mt="5" />
</Box> </Box>
)} )}

@ -111,3 +111,18 @@ func IsOrgAdmin(userID string, orgID string) bool {
return role.IsAdmin() return role.IsAdmin()
} }
func UserInOrg(userID string, orgID string) bool {
var role models.RoleType
err := db.Conn.QueryRow("SELECT role FROM org_member WHERE org_id=? and user_id=?", orgID, userID).Scan(&role)
if err != nil && err != sql.ErrNoRows {
logger.Warn("check is org admin error", "error", err)
return false
}
if err == sql.ErrNoRows {
return false
}
return true
}

@ -66,6 +66,7 @@ var sqlTables = map[string]string{
id VARCHAR(255) PRIMARY KEY, id VARCHAR(255) PRIMARY KEY,
type VARCHAR(1) NOT NULL, type VARCHAR(1) NOT NULL,
creator VARCHAR(255) NOT NULL, creator VARCHAR(255) NOT NULL,
owner VARCHAR(255) DEFAULT '',
slug VARCHAR(64) DEFAULT '', slug VARCHAR(64) DEFAULT '',
title VARCHAR(255) DEFAULT '', title VARCHAR(255) DEFAULT '',
md TEXT DEFAULT '', md TEXT DEFAULT '',
@ -80,6 +81,8 @@ var sqlTables = map[string]string{
ON story (type); ON story (type);
CREATE INDEX IF NOT EXISTS story_creator CREATE INDEX IF NOT EXISTS story_creator
ON story (creator); ON story (creator);
CREATE INDEX IF NOT EXISTS story_owner
ON story (owner);
CREATE INDEX IF NOT EXISTS story_created CREATE INDEX IF NOT EXISTS story_created
ON story (created); ON story (created);
`, `,

@ -11,6 +11,7 @@ import (
"github.com/asaskevich/govalidator" "github.com/asaskevich/govalidator"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/imdotdev/im.dev/server/internal/interaction" "github.com/imdotdev/im.dev/server/internal/interaction"
"github.com/imdotdev/im.dev/server/internal/org"
"github.com/imdotdev/im.dev/server/internal/tags" "github.com/imdotdev/im.dev/server/internal/tags"
"github.com/imdotdev/im.dev/server/internal/user" "github.com/imdotdev/im.dev/server/internal/user"
"github.com/imdotdev/im.dev/server/pkg/config" "github.com/imdotdev/im.dev/server/pkg/config"
@ -70,6 +71,13 @@ func SubmitStory(c *gin.Context) (map[string]string, *e.Error) {
} }
} }
// check user is in org exist
if post.OwnerID != "" {
if !org.UserInOrg(user.ID, post.OwnerID) {
return nil, e.New(http.StatusForbidden, e.NoEditorPermission)
}
}
now := time.Now() now := time.Now()
md := utils.Compress(post.Md) md := utils.Compress(post.Md)
@ -83,8 +91,8 @@ func SubmitStory(c *gin.Context) (map[string]string, *e.Error) {
} }
//create //create
_, err := db.Conn.Exec("INSERT INTO story (id,type,creator,slug, title, md, url, cover, brief,status, created, updated) VALUES(?,?,?,?,?,?,?,?,?,?,?,?)", _, err := db.Conn.Exec("INSERT INTO story (id,type,creator,owner,slug, title, md, url, cover, brief,status, created, updated) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?)",
post.ID, post.Type, user.ID, post.Slug, post.Title, md, post.URL, post.Cover, post.Brief, models.StatusPublished, now, now) post.ID, post.Type, user.ID, post.OwnerID, post.Slug, post.Title, md, post.URL, post.Cover, post.Brief, models.StatusPublished, now, now)
if err != nil { if err != nil {
logger.Warn("submit post error", "error", err) logger.Warn("submit post error", "error", err)
return nil, e.New(http.StatusInternalServerError, e.Internal) return nil, e.New(http.StatusInternalServerError, e.Internal)
@ -96,8 +104,8 @@ func SubmitStory(c *gin.Context) (map[string]string, *e.Error) {
return nil, e.New(http.StatusForbidden, e.NoEditorPermission) return nil, e.New(http.StatusForbidden, e.NoEditorPermission)
} }
_, err = db.Conn.Exec("UPDATE story SET slug=?, title=?, md=?, url=?, cover=?, brief=?, updated=? WHERE id=?", _, err = db.Conn.Exec("UPDATE story SET owner=?, slug=?, title=?, md=?, url=?, cover=?, brief=?, updated=? WHERE id=?",
post.Slug, post.Title, md, post.URL, post.Cover, post.Brief, now, post.ID) post.OwnerID, post.Slug, post.Title, md, post.URL, post.Cover, post.Brief, now, post.ID)
if err != nil { if err != nil {
logger.Warn("upate post error", "error", err) logger.Warn("upate post error", "error", err)
return nil, e.New(http.StatusInternalServerError, e.Internal) return nil, e.New(http.StatusInternalServerError, e.Internal)
@ -205,8 +213,8 @@ func DeletePost(id string) *e.Error {
func GetStory(id string, slug string) (*models.Story, *e.Error) { func GetStory(id string, slug string) (*models.Story, *e.Error) {
ar := &models.Story{} ar := &models.Story{}
var rawmd []byte var rawmd []byte
err := db.Conn.QueryRow("select id,type,slug,title,md,url,cover,brief,creator,status,created,updated from story where id=?", id).Scan( err := db.Conn.QueryRow("select id,type,slug,title,md,url,cover,brief,creator,owner,status,created,updated from story where id=?", id).Scan(
&ar.ID, &ar.Type, &ar.Slug, &ar.Title, &rawmd, &ar.URL, &ar.Cover, &ar.Brief, &ar.CreatorID, &ar.Status, &ar.Created, &ar.Updated, &ar.ID, &ar.Type, &ar.Slug, &ar.Title, &rawmd, &ar.URL, &ar.Cover, &ar.Brief, &ar.CreatorID, &ar.OwnerID, &ar.Status, &ar.Created, &ar.Updated,
) )
if err != nil { if err != nil {
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
@ -218,8 +226,13 @@ func GetStory(id string, slug string) (*models.Story, *e.Error) {
md, _ := utils.Uncompress(rawmd) md, _ := utils.Uncompress(rawmd)
ar.Md = string(md) ar.Md = string(md)
ar.Creator = &models.UserSimple{ID: ar.CreatorID} ar.Creator = &models.UserSimple{ID: ar.CreatorID}
err = ar.Creator.Query() err = ar.Creator.Query()
if ar.OwnerID != "" {
ar.Owner = &models.UserSimple{ID: ar.OwnerID}
err = ar.Owner.Query()
}
// get tags // get tags
t, rawTags, err := tags.GetTargetTags(ar.ID) t, rawTags, err := tags.GetTargetTags(ar.ID)

@ -14,7 +14,7 @@ import (
"github.com/imdotdev/im.dev/server/pkg/models" "github.com/imdotdev/im.dev/server/pkg/models"
) )
const PostQueryPrefix = "select id,type,slug,title,url,cover,brief,creator,created,updated from story " const PostQueryPrefix = "select id,type,slug,title,url,cover,brief,creator,owner,created,updated from story "
func HomePosts(user *models.User, filter string) (models.Stories, *e.Error) { func HomePosts(user *models.User, filter string) (models.Stories, *e.Error) {
@ -144,7 +144,7 @@ func GetPosts(user *models.User, rows *sql.Rows) models.Stories {
posts := make(models.Stories, 0) posts := make(models.Stories, 0)
for rows.Next() { for rows.Next() {
ar := &models.Story{} ar := &models.Story{}
err := rows.Scan(&ar.ID, &ar.Type, &ar.Slug, &ar.Title, &ar.URL, &ar.Cover, &ar.Brief, &ar.CreatorID, &ar.Created, &ar.Updated) err := rows.Scan(&ar.ID, &ar.Type, &ar.Slug, &ar.Title, &ar.URL, &ar.Cover, &ar.Brief, &ar.CreatorID, &ar.OwnerID, &ar.Created, &ar.Updated)
if err != nil { if err != nil {
logger.Warn("scan post error", "error", err) logger.Warn("scan post error", "error", err)
continue continue
@ -155,6 +155,12 @@ func GetPosts(user *models.User, rows *sql.Rows) models.Stories {
creator.Query() creator.Query()
ar.Creator = creator ar.Creator = creator
if ar.OwnerID != "" {
owner := &models.UserSimple{ID: ar.OwnerID}
owner.Query()
ar.Owner = owner
}
// 获取评论信息 // 获取评论信息
ar.Comments = GetCommentCount(ar.ID) ar.Comments = GetCommentCount(ar.ID)

@ -17,6 +17,8 @@ type Story struct {
Type string `json:"type"` Type string `json:"type"`
Creator *UserSimple `json:"creator"` Creator *UserSimple `json:"creator"`
CreatorID string `json:"creatorId"` CreatorID string `json:"creatorId"`
Owner *UserSimple `json:"owner"`
OwnerID string `json:"ownerId"`
Title string `json:"title"` Title string `json:"title"`
Slug string `json:"slug"` Slug string `json:"slug"`
Md string `json:"md"` Md string `json:"md"`

@ -21,8 +21,14 @@ export const StoryAuthor= ({story,showFooter=true,size='lg'}:Props) =>{
<HStack spacing="4"> <HStack spacing="4">
<Avatar src={story.creator.avatar} size={size} onClick={() => router.push(`/${story.creator.username}`)} cursor="pointer"/> <Avatar src={story.creator.avatar} size={size} onClick={() => router.push(`/${story.creator.username}`)} cursor="pointer"/>
<VStack alignItems="left" spacing="1"> <VStack alignItems="left" spacing="1">
<Heading size="sm" onClick={() => router.push(`/${story.creator.username}`)} cursor="pointer">{story.creator.nickname === "" ? story.creator.username : story.creator.nickname}</Heading> {story.ownerId ?
<Text layerStyle="textSecondary" fontSize={size==='lg' ? ".9rem" : ".8rem"}><chakra.span fontWeight="600" ml="1">{moment(story.created).fromNow()}</chakra.span></Text> <HStack spacing={size==='lg'?2:1}>
<Link href={`/${story.creator.username}`}><Text cursor="pointer">{story.creator.nickname}</Text></Link>
<Text layerStyle="textSecondary">for</Text>
<Link href={`/${story.owner.username}`}><Text cursor="pointer">{story.owner.nickname}</Text></Link>
</HStack> :
<Heading size="sm" onClick={() => router.push(`/${story.creator.username}`)} cursor="pointer">{story.creator.nickname === "" ? story.creator.username : story.creator.nickname}</Heading>}
<Text layerStyle="textSecondary" fontSize={size==='lg' ? ".9rem" : ".8rem"}><chakra.span fontWeight="600">{moment(story.created).fromNow()}</chakra.span></Text>
{showFooter && <HStack layerStyle="textSecondary" fontSize=".9rem" spacing="3"> {showFooter && <HStack layerStyle="textSecondary" fontSize=".9rem" spacing="3">
<FaGithub /> <chakra.span>4 min read</chakra.span> <FaGithub /> <chakra.span>4 min read</chakra.span>
</HStack>} </HStack>}

@ -14,6 +14,8 @@ export interface Story {
slug?: string slug?: string
creator?: UserSimple creator?: UserSimple
creatorId?: string creatorId?: string
owner?: UserSimple
ownerId?:string
title?: string title?: string
md?: string md?: string
url?: string url?: string

Loading…
Cancel
Save