pull/52/head
sunface 4 years ago
parent aa205c0086
commit 00797230ad

@ -8,7 +8,8 @@ import { Story } from "src/types/story"
import { useRouter } from "next/router"
import PageContainer1 from "layouts/page-container1"
import Empty from "components/empty"
import TextStoryCard from "components/story/text-story-card"
import TextStoryCard from "components/story/manage-story-card"
import ManageStories from "components/story/manage-stories"
const PostsPage = () => {
const [posts, setPosts] = useState([])
@ -50,17 +51,10 @@ const PostsPage = () => {
posts.length === 0 ?
<Empty />
:
<>
<VStack mt="4">
{posts.map(post =>
<Box width="100%" key={post.id}>
<TextStoryCard story={post} showActions={true} mt="4" onEdit={() => editPost(post)} onDelete={() => onDeletePost(post.id)} showSource={false}/>
<Divider mt="5" />
</Box>
)}
</VStack>
<Center><Text layerStyle="textSecondary" fontSize="sm" mt="5"></Text></Center>
</>
<Box mt="4">
<ManageStories showSource stories={posts} onEdit={(story) => editPost(story)} onDelete={(id) => onDeletePost(id)} />
</Box>
}
</Card>
</Box>

@ -1,13 +1,13 @@
import { Menu,MenuButton,MenuList,MenuItem, Text, Box, Heading, Image, HStack, Center, Button, Flex, Modal, ModalOverlay, ModalContent, ModalHeader, ModalBody, ModalFooter, FormControl, FormLabel, FormHelperText, Input, FormErrorMessage, VStack, Textarea, Divider, useColorModeValue, useToast } from "@chakra-ui/react"
import { Menu, MenuButton, MenuList, MenuItem, Text, Box, Heading, Image, HStack, Center, Button, Flex, Modal, ModalOverlay, ModalContent, ModalHeader, ModalBody, ModalFooter, FormControl, FormLabel, FormHelperText, Input, FormErrorMessage, VStack, Textarea, Divider, useColorModeValue, useToast } from "@chakra-ui/react"
import Card from "components/card"
import Sidebar from "layouts/sidebar/sidebar"
import React, { useEffect, useState } from "react"
import {editorLinks} from "src/data/links"
import { editorLinks } from "src/data/links"
import { requestApi } from "utils/axios/request"
import { useDisclosure } from "@chakra-ui/react"
import { Field, Form, Formik } from "formik"
import { config } from "configs/config"
import TextStoryCard from "components/story/text-story-card"
import TextStoryCard from "components/story/manage-story-card"
import { Story } from "src/types/story"
import { FaExternalLinkAlt, FaRegEdit } from "react-icons/fa"
import { useRouter } from "next/router"
@ -16,9 +16,10 @@ import Link from "next/link"
import PageContainer1 from "layouts/page-container1"
import Empty from "components/empty"
import { IDType } from "src/types/id"
import ManageStories from "components/story/manage-stories"
var validator = require('validator');
const newPost: Story = {type: IDType.Post,title: '', url: '', cover: '' }
const newPost: Story = { type: IDType.Post, title: '', url: '', cover: '' }
const PostsPage = () => {
const [currentPost, setCurrentPost] = useState(newPost)
const [posts, setPosts] = useState([])
@ -85,7 +86,7 @@ const PostsPage = () => {
}
}
const onDeletePost= async (id) => {
const onDeletePost = async (id) => {
await requestApi.delete(`/story/post/${id}`)
getPosts()
toast({
@ -104,20 +105,20 @@ const PostsPage = () => {
<>
<PageContainer1 >
<Box display="flex">
<Sidebar routes={editorLinks} title="创作中心"/>
<Sidebar routes={editorLinks} title="创作中心" />
<Card ml="4" p="6" width="100%">
<Flex alignItems="center" justify="space-between">
<Heading size="md">({posts.length})</Heading>
{config.posts.writingEnabled ?
<Menu>
<MenuButton as={Button} colorScheme="teal" size="sm" _focus={null}>
<MenuButton as={Button} colorScheme="teal" size="sm" _focus={null}>
</MenuButton>
<MenuList color={useColorModeValue("gray.500","gray.400")}>
<MenuItem icon={<FaExternalLinkAlt fontSize="14" />} onClick={onOpen}></MenuItem>
<Link href={`${ReserveUrls.Editor}/post/new`}><MenuItem icon={<FaRegEdit fontSize="16" />} ></MenuItem></Link>
</MenuList>
</Menu>
<MenuList color={useColorModeValue("gray.500", "gray.400")}>
<MenuItem icon={<FaExternalLinkAlt fontSize="14" />} onClick={onOpen}></MenuItem>
<Link href={`${ReserveUrls.Editor}/post/new`}><MenuItem icon={<FaRegEdit fontSize="16" />} ></MenuItem></Link>
</MenuList>
</Menu>
:
<Button colorScheme="teal" size="sm" onClick={onOpen} _focus={null}></Button>}
</Flex>
@ -125,17 +126,9 @@ const PostsPage = () => {
posts.length === 0 ?
<Empty />
:
<>
<VStack mt="4">
{posts.map(post =>
<Box width="100%" key={post.id}>
<TextStoryCard story={post} showActions={true} mt="4" onEdit={() => editPost(post)} onDelete={() => onDeletePost(post.id)} onPin={() => onPinPost(post.id)} />
<Divider mt="5" />
</Box>
)}
</VStack>
<Center><Text layerStyle="textSecondary" fontSize="sm" mt="5"></Text></Center>
</>
<Box mt="4">
<ManageStories showSource stories={posts} onEdit={(story) => editPost(story)} onDelete={(id) => onDeletePost(id)} onPin={(id) => onPinPost(id)} />
</Box>
}
</Card>
</Box>

@ -7,7 +7,7 @@ import { requestApi } from "utils/axios/request"
import { useDisclosure } from "@chakra-ui/react"
import { Field, Form, Formik } from "formik"
import { config } from "configs/config"
import TextStoryCard from "components/story/text-story-card"
import TextStoryCard from "components/story/manage-story-card"
import { Story } from "src/types/story"
import { FaExternalLinkAlt, FaPlus, FaRegEdit } from "react-icons/fa"
import { useRouter } from "next/router"
@ -20,10 +20,11 @@ import PostSelect from "components/story/post-select"
import { cloneDeep, find, remove } from "lodash"
import userCustomTheme from "theme/user-custom"
import Tags from "components/tags/tags"
import ManageStories from "components/story/manage-stories"
var validator = require('validator');
const newSeries: Story = { title: '', brief: '', cover: '', type: IDType.Series }
const PostsPage = () => {
const SeriesPage = () => {
const [currentSeries, setCurrentSeries]: [Story, any] = useState(null)
const [series, setSeries] = useState([])
const [posts, setPosts] = useState([])
@ -286,17 +287,9 @@ const PostsPage = () => {
{
series.length === 0 ? <Empty />
:
<>
<VStack mt="4">
{series.map(post =>
<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)} />
<Divider mt="5" />
</Box>
)}
</VStack>
<Center><Text layerStyle="textSecondary" fontSize="sm" mt="5"></Text></Center>
</>
<Box pt="4">
<ManageStories stories={series} onEdit={(story) => editSeries(story)} onDelete={(id) => onDeleteSeries(id)} showSource={false} onPin={(id) => onPinPost(id)}/>
</Box>
}
</>}
</Card>
@ -305,5 +298,5 @@ const PostsPage = () => {
</>
)
}
export default PostsPage
export default SeriesPage

@ -82,7 +82,7 @@ const UserProfilePage = () => {
<>
<PageContainer>
<Box display="flex">
<Sidebar routes={orgSettingLinks(router.query.org_id)} width={["120px", "120px", "250px", "250px"]} height="fit-content" title={`管理${org?.nickname}`} />
<Sidebar routes={orgSettingLinks(router.query.org_id)} width={["120px", "120px", "250px", "250px"]} height="fit-content" title={`组织${org?.nickname}`} />
<Box ml={[1,1,4,4]} width="100%">
<Card>
<Heading size="sm">Grow the org</Heading>
@ -91,7 +91,7 @@ const UserProfilePage = () => {
<Text>1. Sign in</Text>
<Text>2. Navigate to {config.uiDomain}/settings/orgs</Text>
<Text>3. Paste the secret code below and click Join Organization</Text>
<Tag wordBreak="break-word">{secret}</Tag>
<Tag wordBreak="break-word" maxW="fit-content">{secret}</Tag>
</VStack>
<HStack mt="4">
<Button variant="outline" onClick={generateSecret} _focus={null}>Generate new secret</Button>

@ -0,0 +1,74 @@
import { Text, Box, VStack, Divider, useToast, Heading, Alert, Tag, Button, HStack, Modal, ModalOverlay, ModalContent, ModalBody, Select, useDisclosure, Flex } from "@chakra-ui/react"
import Card from "components/card"
import Nav from "layouts/nav/nav"
import PageContainer from "layouts/page-container"
import Sidebar from "layouts/sidebar/sidebar"
import React, { useEffect, useState } from "react"
import { orgSettingLinks } from "src/data/links"
import { requestApi } from "utils/axios/request"
import { useRouter } from "next/router"
import { User } from "src/types/user"
import UserCard from "components/users/user-card"
import { config } from "configs/config"
import OrgMember from "components/users/org-member"
import { Role } from "src/types/role"
import { cloneDeep } from "lodash"
import { Story } from "src/types/story"
import Empty from "components/empty"
import ManageStories from "components/story/manage-stories"
import { IDType } from "src/types/id"
const OrgPostsPage = () => {
const [org, setOrg]:[User,any] = useState(null)
const [posts,setPosts]:[Story[],any] = useState([])
const router = useRouter()
useEffect(() => {
if (router.query.org_id) {
getPosts()
requestApi.get(`/user/info/${router.query.org_id}`).then(res => setOrg(res.data))
}
}, [router.query.org_id])
const getPosts = async () => {
const res = await requestApi.get(`/story/posts/org/${router.query.org_id}?type=${IDType.Post}`)
setPosts(res.data)
}
const toast = useToast()
const onDeletePost = async id => {
await requestApi.delete(`/org/post/${router.query.org_id}/${id}`)
getPosts()
}
const onPinPost = async id => {
await requestApi.post(`/org/pin/story/${id}`)
}
return (
<>
<PageContainer>
<Box display="flex">
<Sidebar routes={orgSettingLinks(router.query.org_id)} width={["120px", "120px", "250px", "250px"]} height="fit-content" title={`组织${org?.nickname}`} />
<Card ml="4" p="6" width="100%">
<Flex alignItems="center" justify="space-between">
<Heading size="md">({posts.length})</Heading>
</Flex>
{
posts.length === 0 ?
<Empty />
:
<Box mt="4">
<ManageStories showSource stories={posts} onDelete={(id) => onDeletePost(id)} onPin={(id) => onPinPost(id)} />
</Box>
}
</Card>
</Box>
</PageContainer>
</>
)
}
export default OrgPostsPage

@ -116,7 +116,7 @@ const UserProfilePage = () => {
<>
<PageContainer>
<Box display="flex">
<Sidebar routes={orgSettingLinks(router.query.org_id)} width={["120px", "120px", "250px", "250px"]} height="fit-content" title={`管理${org?.nickname}`} />
<Sidebar routes={orgSettingLinks(router.query.org_id)} width={["120px", "120px", "250px", "250px"]} height="fit-content" title={`组织${org?.nickname}`} />
{org && <VStack alignItems="left" ml="4" width="100%">
<Formik
initialValues={org}

@ -5,6 +5,7 @@ import (
"github.com/gin-gonic/gin"
"github.com/imdotdev/im.dev/server/internal/org"
"github.com/imdotdev/im.dev/server/internal/story"
"github.com/imdotdev/im.dev/server/internal/user"
"github.com/imdotdev/im.dev/server/pkg/common"
"github.com/imdotdev/im.dev/server/pkg/db"
@ -323,3 +324,51 @@ func LeaveOrg(c *gin.Context) {
c.JSON(http.StatusOK, common.RespSuccess(nil))
}
func DeleteOrgPost(c *gin.Context) {
orgID := c.Param("orgID")
postID := c.Param("postID")
u := user.CurrentUser(c)
if !org.IsOrgAdmin(u.ID, orgID) {
c.JSON(http.StatusForbidden, common.RespError(e.NoAdminPermission))
return
}
err0 := org.DeletePost(postID)
if err0 != nil {
c.JSON(err0.Status, common.RespError(err0.Message))
return
}
c.JSON(http.StatusOK, common.RespSuccess(nil))
}
func PinOrgStory(c *gin.Context) {
storyID := c.Param("id")
u := user.CurrentUser(c)
s, err := story.GetStory(storyID, "")
if err != nil {
c.JSON(err.Status, common.RespError(err.Message))
return
}
if s.OwnerID == "" {
c.JSON(http.StatusBadRequest, common.RespError("找不到文章关联的组织"))
return
}
if !org.IsOrgAdmin(u.ID, s.OwnerID) {
c.JSON(http.StatusForbidden, common.RespError(e.NoAdminPermission))
return
}
err = story.PinStory(storyID, s.OwnerID)
if err != nil {
c.JSON(err.Status, common.RespError(err.Message))
return
}
c.JSON(http.StatusOK, common.RespSuccess(nil))
}

@ -206,3 +206,13 @@ func Delete(orgID, memberID string) *e.Error {
return nil
}
func DeletePost(postID string) *e.Error {
_, err := db.Conn.Exec("UPDATE story SET owner=? WHERE id=?", "", postID)
if err != nil {
logger.Warn("delete org post error", "error", err)
return e.New(http.StatusInternalServerError, e.Internal)
}
return nil
}

@ -115,6 +115,8 @@ func (s *Server) Start() error {
r.POST("/org/transfer", IsLogin(), api.TransferOrg)
r.DELETE("/org/member/:orgID/:memberID", IsLogin(), api.DeleteOrgMember)
r.POST("/org/leave/:orgID", IsLogin(), api.LeaveOrg)
r.DELETE("/org/post/:orgID/:postID", api.DeleteOrgPost)
r.POST("/org/pin/story/:id", IsLogin(), api.PinOrgStory)
// admin apis
r.POST("/admin/user", IsLogin(), api.AdminSubmitUser)
r.GET("/admin/user/all", IsLogin(), api.AdminGetUsers)

@ -86,7 +86,7 @@ func OrgPosts(tp string, user *models.User, orgID string) (models.Stories, *e.Er
unpinned := make([]*models.Story, 0)
for _, post := range posts {
post.Pinned = GetPinned(post.ID, user.ID)
post.Pinned = GetPinned(post.ID, post.OwnerID)
if post.Pinned {
pinned = append(pinned, post)
} else {

@ -20,6 +20,7 @@ const (
ParamInvalid = "请求参数不正确"
NotFound = "目标不存在"
NoPermission = "你没有权限执行此操作"
NoAdminPermission = "你需要管理员权限"
BadRequest = "非法操作"
AlreadyExist = "目标已经存在"
)

@ -0,0 +1,46 @@
import React from "react"
import { Box, Center, Text, useColorModeValue, VStack } from "@chakra-ui/react"
import { Story } from "src/types/story"
import StoryCard from "./story-card"
import userCustomTheme from "theme/user-custom"
import ManageStoryCard from "./manage-story-card"
interface Props {
stories: Story[]
showFooter?: boolean
highlight?: string
onEdit?: any
onDelete?: any
onPin?: any
showSource?: boolean
}
export const ManageStories = (props: Props) => {
const { stories,showFooter=true,showSource=false, ...rest} = props
const borderColor = useColorModeValue(userCustomTheme.borderColor.light, userCustomTheme.borderColor.dark)
const showBorder = i => {
if (i < stories.length -1) {
return true
}
if (showFooter) {
return true
} else {
return false
}
}
return (
<>
<VStack alignItems="left">
{stories.map((story,i) =>
<Box py="3" borderBottom={showBorder(i)? `1px solid ${borderColor}`:null} key={story.id} px="1">
<ManageStoryCard showSource={showSource} story={story} highlight={props.highlight} {...rest}/>
</Box>)}
</VStack>
{showFooter && <Center><Text layerStyle="textSecondary" fontSize="sm" pt="4"></Text></Center>}
</>
)
}
export default ManageStories

@ -9,21 +9,20 @@ import { getSvgIcon } from "components/svg-icon"
type Props = PropsOf<typeof chakra.div> & {
story: Story
showActions: boolean
onEdit?: any
onDelete?: any
onPin?: any
showSource?: boolean
}
export const TextStoryCard = (props: Props) => {
const { story, showActions, onEdit, onDelete, showSource = true,onPin, ...rest } = props
// 文章卡片,展示需要被管理的文章
export const ManageStoryCard = (props: Props) => {
const { story, onEdit, onDelete, showSource = true,onPin, ...rest } = props
const [isSmallScreen] = useMediaQuery("(max-width: 768px)")
const Lay = isSmallScreen ? VStack : Flex
const gap = moment(story.created).fromNow()
const showActions = onEdit || onDelete || onPin
return (
//@ts-ignore
<Lay justifyContent="space-between" alignItems={isSmallScreen ? "left" : "center"} {...rest}>
@ -34,40 +33,40 @@ export const TextStoryCard = (props: Props) => {
</Heading>
<Text fontSize=".9rem">{gap}</Text>
</VStack>
{props.showActions && <HStack pt={{ base: 3, md: 0 }} layerStyle="textSecondary">
<Tooltip label={story.pinned? "取消置顶" : "置顶"}>
{showActions && <HStack pt={{ base: 3, md: 0 }} layerStyle="textSecondary">
{onPin && <Tooltip label={story.pinned? "取消置顶" : "置顶"}>
<IconButton
aria-label="a icon button"
variant="ghost"
_focus={null}
icon={<FaPaperclip />}
onClick={onPin}
onClick={() => onPin(story.id)}
color={story.pinned? "teal" : null}
/>
</Tooltip>
</Tooltip>}
<Tooltip label="编辑">
{onEdit&&<Tooltip label="编辑">
<IconButton
aria-label="a icon button"
variant="ghost"
_focus={null}
icon={getSvgIcon("edit", "1rem")}
onClick={onEdit}
onClick={() => onEdit(story)}
/>
</Tooltip>
</Tooltip>}
<Tooltip label="删除">
{onDelete&&<Tooltip label="删除">
<IconButton
aria-label="a icon button"
variant="ghost"
_focus={null}
icon={<FaRegTrashAlt />}
onClick={props.onDelete}
onClick={() => props.onDelete(story.id)}
/>
</Tooltip>
</Tooltip>}
</HStack>}
</Lay>
)
}
export default TextStoryCard
export default ManageStoryCard

@ -113,6 +113,12 @@ export function orgSettingLinks(orgID) {
icon: <FaUserFriends />,
disabled: false
},
{
title: '文章管理',
path: `${ReserveUrls.Settings}/org/posts/${orgID}`,
icon: getSvgIcon("post"),
disabled: false
},
]
}

Loading…
Cancel
Save