Skip to content

Commit

Permalink
first implementation of messages templates
Browse files Browse the repository at this point in the history
  • Loading branch information
ybizeul committed Sep 5, 2024
1 parent c07a567 commit 1969dc4
Show file tree
Hide file tree
Showing 13 changed files with 246 additions and 26 deletions.
9 changes: 4 additions & 5 deletions html/src/Components/MarkdownEditor/MarkdownEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,14 @@ import { FullHeightTextArea } from "./FullHeightTextArea";

interface MarkDownEditorProps {
onChange: (message: string) => void;
markdown: string;
}

export function MarkDownEditor(props: MarkDownEditorProps&BoxComponentProps) {
export function MarkDownEditor(props: MarkDownEditorProps&BoxComponentProps&{children: string}) {
// Initialize props
const { onChange, markdown } = props;
const { onChange, children } = props;

// Initialize state
const [_markdown, setMarkdown] = useState<string>(markdown);
const [markdown, setMarkdown] = useState<string>(children);
const [preview, previewH] = useDisclosure(false);

// Functions
Expand All @@ -32,7 +31,7 @@ interface MarkDownEditorProps {
{preview?
<InputWrapper display="flex" style={{flexDirection:"column"}} label="Message" description="This markdown will be displayed to the user" w="100%">
<Paper flex="1" withBorder mt="5" pt="5.5" px="12" display="flex">
<Message value={_markdown} />
<Message value={markdown} />
</Paper>
</InputWrapper>
:
Expand Down
54 changes: 44 additions & 10 deletions html/src/Components/ShareEditor.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { Share } from "@/hupload";
import { ActionIcon, Box, BoxComponentProps, Button, Flex, Input, NumberInput, rem, SegmentedControl, Stack, TextInput, useMantineTheme } from "@mantine/core";
import { IconChevronLeft, IconChevronRight } from "@tabler/icons-react";
import { Message, Share } from "@/hupload";
import { ActionIcon, Box, BoxComponentProps, Button, Flex, Input, Menu, NumberInput, rem, SegmentedControl, Stack, TextInput, useMantineTheme } from "@mantine/core";
import { IconChevronLeft, IconChevronRight, IconListCheck } from "@tabler/icons-react";
import { useDisclosure, useMediaQuery } from "@mantine/hooks";
import { useState } from "react";
import { useEffect, useState } from "react";
import classes from './ShareEditor.module.css';
import { MarkDownEditor } from "./MarkdownEditor";
import { H } from "@/APIClient";

interface ShareEditorProps {
onChange: (options: Share["options"]) => void;
Expand All @@ -20,12 +21,26 @@ export function ShareEditor(props: ShareEditorProps&BoxComponentProps) {

// Initialize state
const [_options, setOptions] = useState<Share["options"]>(options)
const [messages, setMessages] = useState<string[]>([])

// Initialize hooks
const [showMessage, showMessageH ] = useDisclosure(false);
const theme = useMantineTheme()
const isInBrowser = useMediaQuery('(min-width: +' + theme.breakpoints.xs + ')');

// effects
useEffect(() => {
H.get('/messages').then((res) => {
setMessages(res as string[])
})
},[])

const selectMessage = (index: number) => {
H.get('/messages/'+index).then((res) => {
const m = res as Message
notifyChange({..._options, message: m.message as string})
})
}
// Functions
const notifyChange = (o: Share["options"]) => {
setOptions(o)
Expand Down Expand Up @@ -82,12 +97,31 @@ export function ShareEditor(props: ShareEditorProps&BoxComponentProps) {

{/* Right section */}
{(showMessage||!isInBrowser)&&
<MarkDownEditor
pl={isInBrowser?"sm":"0"}
style={{borderLeft: isInBrowser?"1px solid lightGray":""}}
onChange={(v) => { notifyChange({..._options, message:v}); }}
markdown={_options.message?_options.message:""}
/>
<>
<MarkDownEditor
pl={isInBrowser?"sm":"0"}
style={{borderLeft: isInBrowser?"1px solid lightGray":""}}
onChange={(v) => { notifyChange({..._options, message:v}); }}
>
{_options.message?_options.message:""}
</MarkDownEditor>
{messages.length>0&&
<Menu withArrow trapFocus={false}>
<Menu.Target>
<ActionIcon size="xs" id="template" variant={"subtle"} m={rem(3)} radius="xl" style={{position:"absolute", top: 12, right: 40}}>
<IconListCheck style={{ width: rem(16), height: rem(16) }} stroke={1.5} />
</ActionIcon>
</Menu.Target>
<Menu.Dropdown>
{messages.map((m, i) => (
<Menu.Item key={i+1} onClick={() => {selectMessage(i+1)}}>
{m}
</Menu.Item>
))}
</Menu.Dropdown>
</Menu>
}
</>
}
</Flex>
<Flex >
Expand Down
4 changes: 4 additions & 0 deletions html/src/hupload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ export interface ItemInfo {
Size: number;
}

export interface Message {
title: string;
message: string;
}

// Utilities

Expand Down
2 changes: 1 addition & 1 deletion hupload/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ require (
github.com/coreos/go-oidc v2.2.1+incompatible
golang.org/x/crypto v0.25.0
golang.org/x/oauth2 v0.21.0
gopkg.in/square/go-jose.v2 v2.6.0
)

require (
Expand All @@ -34,5 +35,4 @@ require (
github.com/aws/smithy-go v1.20.4 // indirect
github.com/pquerna/cachecontrol v0.2.0 // indirect
github.com/stretchr/testify v1.8.2 // indirect
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
)
29 changes: 29 additions & 0 deletions hupload/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,35 @@ func (h *Hupload) postLogin(w http.ResponseWriter, r *http.Request) {
writeSuccessJSON(w, u)
}

func (h *Hupload) getMessages(w http.ResponseWriter, r *http.Request) {
titles := []string{}

for _, m := range h.Config.Values.MessageTemplates {
titles = append(titles, m.Title)
}

writeSuccessJSON(w, titles)
}

var ErrMessageInvalidIndex = errors.New("invalid index")
var ErrMessageIndexOutOfBounds = errors.New("index out of bounds")

func (h *Hupload) getMessage(w http.ResponseWriter, r *http.Request) {
index, err := strconv.Atoi(r.PathValue("index"))
if err != nil {
writeError(w, http.StatusBadRequest, ErrMessageInvalidIndex.Error())
return
}
t := h.Config.Values.MessageTemplates
if len(t) <= index && index > 0 {
writeSuccessJSON(w, t[index-1])
return
} else {
writeError(w, http.StatusBadRequest, ErrMessageIndexOutOfBounds.Error())
return
}
}

// getVersion returns hupload version
func (h *Hupload) getVersion(w http.ResponseWriter, r *http.Request) {
v := struct {
Expand Down
127 changes: 126 additions & 1 deletion hupload/handlers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ func TestCreateShare(t *testing.T) {
return
}

got := string(w.Body.Bytes())
got := string(w.Body.String())
want := `{"errors":["JWTAuthMiddleware: no Authorization header"]}`

if want != got {
Expand Down Expand Up @@ -1492,6 +1492,131 @@ func TestDeleteItem(t *testing.T) {
})
}
}

func TestMessages(t *testing.T) {
c := &config.Config{
Path: "handlers_testdata/config.yml",
}

h := getHupload(t, c)

t.Run("Get messages should work", func(t *testing.T) {
req := httptest.NewRequest("GET", "/api/v1/messages", nil)

req.SetBasicAuth("admin", "hupload")

w := httptest.NewRecorder()

h.API.Mux.ServeHTTP(w, req)

if w.Code != http.StatusOK {
t.Errorf("Expected status %d, got %d", http.StatusOK, w.Code)
return
}

got := []string{}
_ = json.NewDecoder(w.Body).Decode(&got)

want := []string{"Message title"}

if !reflect.DeepEqual(got, want) {
t.Errorf("Expected %v, got %v", want, got)
return
}
})

t.Run("Get message without auth should fail", func(t *testing.T) {
req := httptest.NewRequest("GET", "/api/v1/messages", nil)

w := httptest.NewRecorder()

h.API.Mux.ServeHTTP(w, req)

if w.Code != http.StatusUnauthorized {
t.Errorf("Expected status %d, got %d", http.StatusOK, w.Code)
return
}

type btype struct {
Errors []string `json:"errors"`
}

got := btype{}

_ = json.NewDecoder(w.Body).Decode(&got)

want := btype{
Errors: []string{"JWTAuthMiddleware: no Authorization header"},
}

if !reflect.DeepEqual(got, want) {
t.Errorf("Expected %v, got %v", want, got)
return
}
})

t.Run("Get message should work", func(t *testing.T) {
req := httptest.NewRequest("GET", "/api/v1/messages/1", nil)

req.SetBasicAuth("admin", "hupload")

w := httptest.NewRecorder()

h.API.Mux.ServeHTTP(w, req)

if w.Code != http.StatusOK {
t.Errorf("Expected status %d, got %d", http.StatusOK, w.Code)
return
}

var got *config.MessageTemplate

_ = json.NewDecoder(w.Body).Decode(&got)

want := &config.MessageTemplate{
Title: "Message title",
Message: "Message content",
}

if !reflect.DeepEqual(got, want) {
t.Errorf("Expected %v, got %v", want, got)
return
}
})

t.Run("Get message without auth should fail", func(t *testing.T) {
req := httptest.NewRequest("GET", "/api/v1/messages/1", nil)

req.SetBasicAuth("admin", "hupload")

w := httptest.NewRecorder()

h.API.Mux.ServeHTTP(w, req)

if w.Code != http.StatusUnauthorized {
t.Errorf("Expected status %d, got %d", http.StatusOK, w.Code)
return
}

type btype struct {
Errors []string `json:"errors"`
}

got := btype{}

_ = json.NewDecoder(w.Body).Decode(&got)

want := btype{
Errors: []string{"JWTAuthMiddleware: no Authorization header"},
}

if !reflect.DeepEqual(got, want) {
t.Errorf("Expected %v, got %v", want, got)
return
}
})
}

func TestVersion(t *testing.T) {
t.Cleanup(func() {
os.RemoveAll("tmptest")
Expand Down
6 changes: 5 additions & 1 deletion hupload/handlers_testdata/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,8 @@ storage:
auth:
type: file
options:
path: handlers_testdata/users.yml
path: handlers_testdata/users.yml
messages:
- title: Message title
message: |
Message content
7 changes: 4 additions & 3 deletions hupload/internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,10 @@ type TypeOptions struct {
// ConfigValues.Title.
type ConfigValues struct {
Title string
DefaultValidityDays int `yaml:"availability_days"`
Storage TypeOptions `yaml:"storage"`
Authentication TypeOptions `yaml:"auth"`
DefaultValidityDays int `yaml:"availability_days"`
Storage TypeOptions `yaml:"storage"`
Authentication TypeOptions `yaml:"auth"`
MessageTemplates []MessageTemplate `yaml:"messages"`
}

// Config is the internal representation of Hupload configuration file at path
Expand Down
6 changes: 6 additions & 0 deletions hupload/internal/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,12 @@ func TestLoadGoodConfig(t *testing.T) {
"path": "config_testdata/users.yml",
},
},
MessageTemplates: []MessageTemplate{
{
Title: "Message title",
Message: "Message content",
},
},
}

got := c.Values
Expand Down
4 changes: 4 additions & 0 deletions hupload/internal/config/config_testdata/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,7 @@ storage:
path: data
max_file_mb: 500
max_share_mb: 2000
messages:
- title: Message title
message: |
Message content
6 changes: 6 additions & 0 deletions hupload/internal/config/messages.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package config

type MessageTemplate struct {
Title string `yaml:"title" json:"title"`
Message string `yaml:"message" json:"message"`
}
2 changes: 1 addition & 1 deletion hupload/pkg/apiws/middleware/auth/auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ func TestServeNextAuthenticated(t *testing.T) {
s := r.Context().Value(authentication.AuthStatusKey).(authentication.AuthStatus)
c := s.Error
if c != nil {
t.Errorf("Expected nil, got %v", c.(error))
t.Errorf("Expected nil, got %v", c)
}
if !s.Authenticated {
t.Errorf("Expected AuthStatusSuccess, got %v", c)
Expand Down
Loading

0 comments on commit 1969dc4

Please sign in to comment.