mirror of
https://github.com/sigmasternchen/threadule
synced 2025-03-15 08:09:01 +00:00
preparations for thread add and edit
This commit is contained in:
parent
20e832e9e1
commit
a98a95f311
15 changed files with 257 additions and 23 deletions
|
@ -21,6 +21,7 @@ type Data interface {
|
|||
AddAccount(account *models.Account) error
|
||||
UpdateAccount(account *models.Account) error
|
||||
|
||||
AddThread(thread *models.Thread) error
|
||||
GetScheduledThreads() ([]models.Thread, error)
|
||||
GetTweetsForThread(thread *models.Thread) ([]models.Tweet, error)
|
||||
UpdateThread(thread *models.Thread) error
|
||||
|
|
|
@ -12,4 +12,6 @@ type Logic interface {
|
|||
GetAccounts(user *models.User) ([]models.Account, error)
|
||||
AddAccount(user *models.User) (string, *url.URL, error)
|
||||
AddAccountResolve(user *models.User, id string, pin string) (*models.Account, error)
|
||||
|
||||
AddThread(thread *models.Thread, user *models.User) error
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@ const (
|
|||
type Thread struct {
|
||||
BaseModel
|
||||
AccountID uuid.UUID `json:"-"`
|
||||
Account *Account `json:"-"`
|
||||
Account *Account `json:"account"`
|
||||
Tweets []Tweet `json:"tweets"`
|
||||
|
||||
ScheduledFor time.Time `json:"scheduled_for"`
|
||||
|
|
|
@ -33,3 +33,10 @@ func (d *Data) GetScheduledThreads() ([]models.Thread, error) {
|
|||
Error
|
||||
return threads, err
|
||||
}
|
||||
|
||||
func (d *Data) AddThread(thread *models.Thread) error {
|
||||
return d.db.
|
||||
Omit("Account").
|
||||
Create(thread).
|
||||
Error
|
||||
}
|
||||
|
|
|
@ -7,4 +7,5 @@ var (
|
|||
ErrInvalidSession = errors.New("invalid session")
|
||||
ErrInternalError = errors.New("something went wrong")
|
||||
ErrInvalidParameter = errors.New("invalid parameter")
|
||||
ErrNotFound = errors.New("resource not found")
|
||||
)
|
||||
|
|
26
backend/internal/logic/thread.go
Normal file
26
backend/internal/logic/thread.go
Normal file
|
@ -0,0 +1,26 @@
|
|||
package logic
|
||||
|
||||
import (
|
||||
uuid "github.com/satori/go.uuid"
|
||||
"threadule/backend/internal/data/models"
|
||||
)
|
||||
|
||||
func (l *Logic) AddThread(thread *models.Thread, user *models.User) error {
|
||||
if uuid.Nil == thread.AccountID {
|
||||
if thread.Account == nil {
|
||||
return ErrInvalidParameter
|
||||
}
|
||||
if uuid.Nil == thread.Account.ID {
|
||||
return ErrInvalidParameter
|
||||
}
|
||||
thread.AccountID = thread.Account.ID
|
||||
}
|
||||
|
||||
_, err := l.ctx.Data.GetAccountById(user, thread.AccountID.String())
|
||||
if err != nil {
|
||||
return ErrNotFound
|
||||
}
|
||||
|
||||
err = l.ctx.Data.AddThread(thread)
|
||||
return err
|
||||
}
|
|
@ -24,6 +24,8 @@ func StatusResponse(ctx *web.Context, status int, details string) {
|
|||
|
||||
func ErrorResponse(ctx *web.Context, err error) {
|
||||
switch err {
|
||||
case logic.ErrNotFound:
|
||||
StatusResponse(ctx, http.StatusNotFound, err.Error())
|
||||
case logic.ErrInvalidParameter:
|
||||
StatusResponse(ctx, http.StatusBadRequest, err.Error())
|
||||
case logic.ErrLoginFailed:
|
||||
|
|
27
backend/internal/presentation/thread.go
Normal file
27
backend/internal/presentation/thread.go
Normal file
|
@ -0,0 +1,27 @@
|
|||
package presentation
|
||||
|
||||
import (
|
||||
"threadule/backend/internal/data/models"
|
||||
"threadule/backend/internal/web"
|
||||
)
|
||||
|
||||
func AddThread(ctx *web.Context) {
|
||||
var thread models.Thread
|
||||
err := ctx.ReadJSON(&thread)
|
||||
if err != nil {
|
||||
ErrorResponse(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
err = ctx.AppCtx.Logic.AddThread(&thread, ctx.Session.User)
|
||||
if err != nil {
|
||||
ErrorResponse(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
err = ctx.WriteJSON(&thread)
|
||||
if err != nil {
|
||||
ErrorResponse(ctx, err)
|
||||
return
|
||||
}
|
||||
}
|
|
@ -27,5 +27,7 @@ func Setup(ctx *app.Context) http.Handler {
|
|||
router.POST("/account/", authenticated(AddAccount))
|
||||
router.POST("/account/:id", authenticated(AddAccountResolve))
|
||||
|
||||
router.POST("/thread/", authenticated(AddThread))
|
||||
|
||||
return router
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
"private": true,
|
||||
"dependencies": {
|
||||
"@material-ui/core": "^4.12.3",
|
||||
"@material-ui/icons": "^4.11.2",
|
||||
"@material-ui/lab": "^4.0.0-alpha.60",
|
||||
"@testing-library/jest-dom": "^5.11.4",
|
||||
"@testing-library/react": "^11.1.0",
|
||||
|
|
18
frontend/src/api/endpoints/ThreadEndpoint.ts
Normal file
18
frontend/src/api/endpoints/ThreadEndpoint.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
import {Client} from "../client";
|
||||
import Endpoint from "./Endpoint";
|
||||
import Thread from "../entities/Thread";
|
||||
|
||||
const API_PREFIX = "/api/account/"
|
||||
|
||||
class ThreadEndpoint extends Endpoint {
|
||||
constructor(client: Client) {
|
||||
super(client)
|
||||
this.requireAuthenticated()
|
||||
}
|
||||
|
||||
public async add(thread: Thread): Promise<Thread> {
|
||||
return await this.post<Thread, Thread>(API_PREFIX, thread)
|
||||
}
|
||||
}
|
||||
|
||||
export default ThreadEndpoint
|
|
@ -1,3 +1,4 @@
|
|||
import Tweet from "./Tweet";
|
||||
|
||||
export enum ThreadStatus {
|
||||
SCHEDULED = "SCHEDULED",
|
||||
|
@ -8,10 +9,10 @@ export enum ThreadStatus {
|
|||
|
||||
type Thread = {
|
||||
id: string,
|
||||
//tweets: Tweet[],
|
||||
tweets: Tweet[],
|
||||
scheduled_for: Date,
|
||||
status: ThreadStatus,
|
||||
error: string|null
|
||||
error: string|null,
|
||||
}
|
||||
|
||||
export default Thread
|
16
frontend/src/api/entities/Tweet.ts
Normal file
16
frontend/src/api/entities/Tweet.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
|
||||
export enum TweetStatus {
|
||||
SCHEDULED = "SCHEDULED",
|
||||
FAILED = "FAILED",
|
||||
DONE = "DONE",
|
||||
}
|
||||
|
||||
type Tweet = {
|
||||
id: string,
|
||||
text: string,
|
||||
status: TweetStatus,
|
||||
tweet_id: number|null,
|
||||
error: string|null
|
||||
}
|
||||
|
||||
export default Tweet
|
|
@ -1,37 +1,68 @@
|
|||
import {FunctionComponent} from "react";
|
||||
import {FunctionComponent, useState} from "react";
|
||||
import {Avatar, Card, CardActions, CardContent, CardHeader, IconButton} from "@material-ui/core";
|
||||
import Account from "../../api/entities/Account";
|
||||
import ThreadList from "../ThreadList";
|
||||
|
||||
import AddIcon from '@material-ui/icons/Add';
|
||||
import ThreadFormDialog from "../ThreadFormDialog";
|
||||
import Thread, {ThreadStatus} from "../../api/entities/Thread";
|
||||
|
||||
export type AccountCardProps = {
|
||||
account: Account
|
||||
}
|
||||
|
||||
const emptyThread = () => ({
|
||||
id: "",
|
||||
scheduled_for: new Date(),
|
||||
status: ThreadStatus.SCHEDULED,
|
||||
tweets: [],
|
||||
error: null,
|
||||
})
|
||||
|
||||
const AccountCard: FunctionComponent<AccountCardProps> = ({account}) => {
|
||||
const [editThread, setEditThread] = useState<Thread | null>(null)
|
||||
|
||||
const openNewForm = () => {
|
||||
setEditThread(emptyThread())
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader
|
||||
avatar={
|
||||
<Avatar alt={account.screen_name} src={account.avatar_url}/>
|
||||
}
|
||||
action={
|
||||
<IconButton aria-label="settings">
|
||||
<>
|
||||
<Card>
|
||||
<CardHeader
|
||||
avatar={
|
||||
<Avatar alt={account.screen_name} src={account.avatar_url}/>
|
||||
}
|
||||
action={
|
||||
<IconButton aria-label="settings">
|
||||
</IconButton>
|
||||
}
|
||||
title={account.name}
|
||||
subheader={account.screen_name}
|
||||
/>
|
||||
<CardContent>
|
||||
<ThreadList threads={account.threads}/>
|
||||
</CardContent>
|
||||
<CardActions disableSpacing>
|
||||
<IconButton aria-label="add" onClick={() => {
|
||||
openNewForm()
|
||||
}}>
|
||||
<AddIcon/>
|
||||
</IconButton>
|
||||
}
|
||||
title={account.name}
|
||||
subheader={account.screen_name}
|
||||
</CardActions>
|
||||
</Card>
|
||||
|
||||
<ThreadFormDialog
|
||||
account={account}
|
||||
open={Boolean(editThread)}
|
||||
initial={editThread ? editThread : emptyThread()}
|
||||
onSubmit={(thread) => {
|
||||
account.threads.push(thread)
|
||||
|
||||
setEditThread(null)
|
||||
}}
|
||||
/>
|
||||
<CardContent>
|
||||
<ThreadList threads={account.threads} />
|
||||
</CardContent>
|
||||
<CardActions disableSpacing>
|
||||
<IconButton aria-label="add">
|
||||
<AddIcon/>
|
||||
</IconButton>
|
||||
</CardActions>
|
||||
</Card>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
99
frontend/src/components/ThreadFormDialog/index.tsx
Normal file
99
frontend/src/components/ThreadFormDialog/index.tsx
Normal file
|
@ -0,0 +1,99 @@
|
|||
import {FunctionComponent, useState} from "react";
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
Grid,
|
||||
IconButton,
|
||||
TextField
|
||||
} from "@material-ui/core";
|
||||
import Account from "../../api/entities/Account";
|
||||
import Thread from "../../api/entities/Thread";
|
||||
import AddIcon from "@material-ui/icons/Add";
|
||||
import {TweetStatus} from "../../api/entities/Tweet";
|
||||
|
||||
export type ThreadFormProps = {
|
||||
open: boolean
|
||||
account: Account
|
||||
initial: Thread
|
||||
onSubmit: (thread: Thread) => void
|
||||
}
|
||||
|
||||
const Index: FunctionComponent<ThreadFormProps> = ({open, account, initial, onSubmit}) => {
|
||||
const [thread, setThread] = useState<Thread>(initial)
|
||||
|
||||
return (
|
||||
<Dialog open={open}>
|
||||
<DialogTitle title={"Thread"}/>
|
||||
<DialogContent>
|
||||
<Grid container spacing={2}>
|
||||
<Grid item xs={12}>
|
||||
<TextField
|
||||
id="datetime-local"
|
||||
label="Scheduled For"
|
||||
type="datetime-local"
|
||||
value={thread.scheduled_for.toISOString().replace(/:[0-9]{2}\.[0-9]{3}.*/, "")}
|
||||
onChange={event => {
|
||||
setThread({
|
||||
...thread,
|
||||
scheduled_for: new Date(event.target.value)
|
||||
})
|
||||
}}
|
||||
InputLabelProps={{
|
||||
shrink: true,
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
{
|
||||
thread.tweets.map((tweet, index) => (
|
||||
<Grid item xs={12}>
|
||||
<TextField
|
||||
id="outlined-multiline-static"
|
||||
label={"Tweet " + (index + 1)}
|
||||
multiline
|
||||
rows={3}
|
||||
value={tweet.text}
|
||||
onChange={event => {
|
||||
thread.tweets[index].text = event.target.value
|
||||
setThread({
|
||||
...thread
|
||||
})
|
||||
}}
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
/>
|
||||
</Grid>
|
||||
))
|
||||
}
|
||||
<Grid item xs={12}>
|
||||
<IconButton aria-label="add" onClick={() => {
|
||||
thread.tweets.push({
|
||||
id: "",
|
||||
text: "",
|
||||
status: TweetStatus.SCHEDULED,
|
||||
tweet_id: null,
|
||||
error: null
|
||||
})
|
||||
setThread({
|
||||
...thread
|
||||
})
|
||||
}}>
|
||||
<AddIcon/>
|
||||
</IconButton>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => {
|
||||
onSubmit(thread)
|
||||
}} color="primary">
|
||||
Save changes
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
export default Index
|
Loading…
Reference in a new issue