feat: make edit visibility a toggle (#277)

* feat: make edit visibility a toggle

Signed-off-by: jolheiser <john.olheiser@gmail.com>

* Tweak SVG dropdown icon size & color

---------

Signed-off-by: jolheiser <john.olheiser@gmail.com>
Co-authored-by: Thomas Miceli <tho.miceli@gmail.com>
This commit is contained in:
John Olheiser 2024-05-11 14:03:25 -05:00 committed by GitHub
parent 97636b23f5
commit 2fd053a077
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 102 additions and 66 deletions

View file

@ -538,13 +538,17 @@ func (gist *Gist) GetLanguagesFromFiles() ([]string, error) {
// -- DTO -- // // -- DTO -- //
type GistDTO struct { type GistDTO struct {
Title string `validate:"max=250" form:"title"` Title string `validate:"max=250" form:"title"`
Description string `validate:"max=1000" form:"description"` Description string `validate:"max=1000" form:"description"`
URL string `validate:"max=32,alphanumdashorempty" form:"url"` URL string `validate:"max=32,alphanumdashorempty" form:"url"`
Private Visibility `validate:"number,min=0,max=2" form:"private"` Files []FileDTO `validate:"min=1,dive"`
Files []FileDTO `validate:"min=1,dive"` Name []string `form:"name"`
Name []string `form:"name"` Content []string `form:"content"`
Content []string `form:"content"` VisibilityDTO
}
type VisibilityDTO struct {
Private Visibility `validate:"number,min=0,max=2" form:"private"`
} }
type FileDTO struct { type FileDTO struct {

View file

@ -6,12 +6,6 @@ import (
"bytes" "bytes"
"errors" "errors"
"fmt" "fmt"
"github.com/rs/zerolog/log"
"github.com/thomiceli/opengist/internal/git"
"github.com/thomiceli/opengist/internal/i18n"
"github.com/thomiceli/opengist/internal/index"
"github.com/thomiceli/opengist/internal/render"
"github.com/thomiceli/opengist/internal/utils"
"html/template" "html/template"
"net/url" "net/url"
"path/filepath" "path/filepath"
@ -20,6 +14,13 @@ import (
"strings" "strings"
"time" "time"
"github.com/rs/zerolog/log"
"github.com/thomiceli/opengist/internal/git"
"github.com/thomiceli/opengist/internal/i18n"
"github.com/thomiceli/opengist/internal/index"
"github.com/thomiceli/opengist/internal/render"
"github.com/thomiceli/opengist/internal/utils"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
"github.com/thomiceli/opengist/internal/config" "github.com/thomiceli/opengist/internal/config"
@ -603,10 +604,15 @@ func processCreate(ctx echo.Context) error {
return redirect(ctx, "/"+user.Username+"/"+gist.Identifier()) return redirect(ctx, "/"+user.Username+"/"+gist.Identifier())
} }
func toggleVisibility(ctx echo.Context) error { func editVisibility(ctx echo.Context) error {
gist := getData(ctx, "gist").(*db.Gist) gist := getData(ctx, "gist").(*db.Gist)
gist.Private = (gist.Private + 1) % 3 dto := new(db.VisibilityDTO)
if err := ctx.Bind(dto); err != nil {
return errorRes(400, tr(ctx, "error.cannot-bind-data"), err)
}
gist.Private = dto.Private
if err := gist.UpdateNoTimestamps(); err != nil { if err := gist.UpdateNoTimestamps(); err != nil {
return errorRes(500, "Error updating this gist", err) return errorRes(500, "Error updating this gist", err)
} }
@ -733,7 +739,6 @@ func downloadFile(ctx echo.Context) error {
ctx.Response().Header().Set("Content-Disposition", "attachment; filename="+file.Filename) ctx.Response().Header().Set("Content-Disposition", "attachment; filename="+file.Filename)
ctx.Response().Header().Set("Content-Length", strconv.Itoa(len(file.Content))) ctx.Response().Header().Set("Content-Length", strconv.Itoa(len(file.Content)))
_, err = ctx.Response().Write([]byte(file.Content)) _, err = ctx.Response().Write([]byte(file.Content))
if err != nil { if err != nil {
return errorRes(500, "Error downloading the file", err) return errorRes(500, "Error downloading the file", err)
} }

View file

@ -5,9 +5,6 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"github.com/thomiceli/opengist/internal/index"
"github.com/thomiceli/opengist/internal/utils"
"github.com/thomiceli/opengist/templates"
htmlpkg "html" htmlpkg "html"
"html/template" "html/template"
"io" "io"
@ -21,6 +18,10 @@ import (
"strings" "strings"
"time" "time"
"github.com/thomiceli/opengist/internal/index"
"github.com/thomiceli/opengist/internal/utils"
"github.com/thomiceli/opengist/templates"
"github.com/gorilla/sessions" "github.com/gorilla/sessions"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware" "github.com/labstack/echo/v4/middleware"
@ -313,7 +314,7 @@ func NewServer(isDev bool) *Server {
g3.GET("/rev/:revision", gistIndex) g3.GET("/rev/:revision", gistIndex)
g3.GET("/revisions", revisions) g3.GET("/revisions", revisions)
g3.GET("/archive/:revision", downloadZip) g3.GET("/archive/:revision", downloadZip)
g3.POST("/visibility", toggleVisibility, logged, writePermission) g3.POST("/visibility", editVisibility, logged, writePermission)
g3.POST("/delete", deleteGist, logged, writePermission) g3.POST("/delete", deleteGist, logged, writePermission)
g3.GET("/raw/:revision/:file", rawFile) g3.GET("/raw/:revision/:file", rawFile)
g3.GET("/download/:revision/:file", downloadFile) g3.GET("/download/:revision/:file", downloadFile)

View file

@ -1,10 +1,11 @@
package test package test
import ( import (
"testing"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/thomiceli/opengist/internal/db" "github.com/thomiceli/opengist/internal/db"
"github.com/thomiceli/opengist/internal/git" "github.com/thomiceli/opengist/internal/git"
"testing"
) )
func TestGists(t *testing.T) { func TestGists(t *testing.T) {
@ -28,9 +29,11 @@ func TestGists(t *testing.T) {
gist1 := db.GistDTO{ gist1 := db.GistDTO{
Title: "gist1", Title: "gist1",
Description: "my first gist", Description: "my first gist",
Private: 0, VisibilityDTO: db.VisibilityDTO{
Name: []string{"gist1.txt", "gist2.txt", "gist3.txt"}, Private: 0,
Content: []string{"yeah", "yeah\ncool", "yeah\ncool gist actually"}, },
Name: []string{"gist1.txt", "gist2.txt", "gist3.txt"},
Content: []string{"yeah", "yeah\ncool", "yeah\ncool gist actually"},
} }
err = s.request("POST", "/", gist1, 302) err = s.request("POST", "/", gist1, 302)
require.NoError(t, err) require.NoError(t, err)
@ -57,9 +60,11 @@ func TestGists(t *testing.T) {
gist2 := db.GistDTO{ gist2 := db.GistDTO{
Title: "gist2", Title: "gist2",
Description: "my second gist", Description: "my second gist",
Private: 0, VisibilityDTO: db.VisibilityDTO{
Name: []string{"", "gist2.txt", "gist3.txt"}, Private: 0,
Content: []string{"", "yeah\ncool", "yeah\ncool gist actually"}, },
Name: []string{"", "gist2.txt", "gist3.txt"},
Content: []string{"", "yeah\ncool", "yeah\ncool gist actually"},
} }
err = s.request("POST", "/", gist2, 200) err = s.request("POST", "/", gist2, 200)
require.NoError(t, err) require.NoError(t, err)
@ -67,9 +72,11 @@ func TestGists(t *testing.T) {
gist3 := db.GistDTO{ gist3 := db.GistDTO{
Title: "gist3", Title: "gist3",
Description: "my third gist", Description: "my third gist",
Private: 0, VisibilityDTO: db.VisibilityDTO{
Name: []string{""}, Private: 0,
Content: []string{"yeah"}, },
Name: []string{""},
Content: []string{"yeah"},
} }
err = s.request("POST", "/", gist3, 302) err = s.request("POST", "/", gist3, 302)
require.NoError(t, err) require.NoError(t, err)
@ -110,9 +117,11 @@ func TestVisibility(t *testing.T) {
gist1 := db.GistDTO{ gist1 := db.GistDTO{
Title: "gist1", Title: "gist1",
Description: "my first gist", Description: "my first gist",
Private: db.UnlistedVisibility, VisibilityDTO: db.VisibilityDTO{
Name: []string{""}, Private: db.UnlistedVisibility,
Content: []string{"yeah"}, },
Name: []string{""},
Content: []string{"yeah"},
} }
err = s.request("POST", "/", gist1, 302) err = s.request("POST", "/", gist1, 302)
require.NoError(t, err) require.NoError(t, err)
@ -121,19 +130,19 @@ func TestVisibility(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, db.UnlistedVisibility, gist1db.Private) require.Equal(t, db.UnlistedVisibility, gist1db.Private)
err = s.request("POST", "/"+gist1db.User.Username+"/"+gist1db.Uuid+"/visibility", nil, 302) err = s.request("POST", "/"+gist1db.User.Username+"/"+gist1db.Uuid+"/visibility", db.VisibilityDTO{Private: db.PrivateVisibility}, 302)
require.NoError(t, err) require.NoError(t, err)
gist1db, err = db.GetGistByID("1") gist1db, err = db.GetGistByID("1")
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, db.PrivateVisibility, gist1db.Private) require.Equal(t, db.PrivateVisibility, gist1db.Private)
err = s.request("POST", "/"+gist1db.User.Username+"/"+gist1db.Uuid+"/visibility", nil, 302) err = s.request("POST", "/"+gist1db.User.Username+"/"+gist1db.Uuid+"/visibility", db.VisibilityDTO{Private: db.PublicVisibility}, 302)
require.NoError(t, err) require.NoError(t, err)
gist1db, err = db.GetGistByID("1") gist1db, err = db.GetGistByID("1")
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, db.PublicVisibility, gist1db.Private) require.Equal(t, db.PublicVisibility, gist1db.Private)
err = s.request("POST", "/"+gist1db.User.Username+"/"+gist1db.Uuid+"/visibility", nil, 302) err = s.request("POST", "/"+gist1db.User.Username+"/"+gist1db.Uuid+"/visibility", db.VisibilityDTO{Private: db.UnlistedVisibility}, 302)
require.NoError(t, err) require.NoError(t, err)
gist1db, err = db.GetGistByID("1") gist1db, err = db.GetGistByID("1")
require.NoError(t, err) require.NoError(t, err)
@ -152,9 +161,11 @@ func TestLikeFork(t *testing.T) {
gist1 := db.GistDTO{ gist1 := db.GistDTO{
Title: "gist1", Title: "gist1",
Description: "my first gist", Description: "my first gist",
Private: 1, VisibilityDTO: db.VisibilityDTO{
Name: []string{""}, Private: 1,
Content: []string{"yeah"}, },
Name: []string{""},
Content: []string{"yeah"},
} }
err = s.request("POST", "/", gist1, 302) err = s.request("POST", "/", gist1, 302)
require.NoError(t, err) require.NoError(t, err)
@ -212,9 +223,11 @@ func TestCustomUrl(t *testing.T) {
Title: "gist1", Title: "gist1",
URL: "my-gist", URL: "my-gist",
Description: "my first gist", Description: "my first gist",
Private: 0, VisibilityDTO: db.VisibilityDTO{
Name: []string{"gist1.txt", "gist2.txt", "gist3.txt"}, Private: 0,
Content: []string{"yeah", "yeah\ncool", "yeah\ncool gist actually"}, },
Name: []string{"gist1.txt", "gist2.txt", "gist3.txt"},
Content: []string{"yeah", "yeah\ncool", "yeah\ncool gist actually"},
} }
err = s.request("POST", "/", gist1, 302) err = s.request("POST", "/", gist1, 302)
require.NoError(t, err) require.NoError(t, err)
@ -241,9 +254,11 @@ func TestCustomUrl(t *testing.T) {
gist2 := db.GistDTO{ gist2 := db.GistDTO{
Title: "gist2", Title: "gist2",
Description: "my second gist", Description: "my second gist",
Private: 0, VisibilityDTO: db.VisibilityDTO{
Name: []string{"gist1.txt", "gist2.txt", "gist3.txt"}, Private: 0,
Content: []string{"yeah", "yeah\ncool", "yeah\ncool gist actually"}, },
Name: []string{"gist1.txt", "gist2.txt", "gist3.txt"},
Content: []string{"yeah", "yeah\ncool", "yeah\ncool gist actually"},
} }
err = s.request("POST", "/", gist2, 302) err = s.request("POST", "/", gist2, 302)
require.NoError(t, err) require.NoError(t, err)

View file

@ -3,13 +3,6 @@ package test
import ( import (
"errors" "errors"
"fmt" "fmt"
"github.com/rs/zerolog/log"
"github.com/stretchr/testify/require"
"github.com/thomiceli/opengist/internal/config"
"github.com/thomiceli/opengist/internal/db"
"github.com/thomiceli/opengist/internal/git"
"github.com/thomiceli/opengist/internal/memdb"
"github.com/thomiceli/opengist/internal/web"
"io" "io"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
@ -21,6 +14,14 @@ import (
"strconv" "strconv"
"strings" "strings"
"testing" "testing"
"github.com/rs/zerolog/log"
"github.com/stretchr/testify/require"
"github.com/thomiceli/opengist/internal/config"
"github.com/thomiceli/opengist/internal/db"
"github.com/thomiceli/opengist/internal/git"
"github.com/thomiceli/opengist/internal/memdb"
"github.com/thomiceli/opengist/internal/web"
) )
type testServer struct { type testServer struct {
@ -106,7 +107,7 @@ func structToURLValues(s interface{}) url.Values {
for i := 0; i < rValue.NumField(); i++ { for i := 0; i < rValue.NumField(); i++ {
field := rValue.Type().Field(i) field := rValue.Type().Field(i)
tag := field.Tag.Get("form") tag := field.Tag.Get("form")
if tag != "" { if tag != "" || field.Anonymous {
if field.Type.Kind() == reflect.Int { if field.Type.Kind() == reflect.Int {
fieldValue := rValue.Field(i).Int() fieldValue := rValue.Field(i).Int()
v.Add(tag, strconv.FormatInt(fieldValue, 10)) v.Add(tag, strconv.FormatInt(fieldValue, 10))
@ -115,6 +116,12 @@ func structToURLValues(s interface{}) url.Values {
for _, va := range fieldValue { for _, va := range fieldValue {
v.Add(tag, va) v.Add(tag, va)
} }
} else if field.Type.Kind() == reflect.Struct {
for key, val := range structToURLValues(rValue.Field(i).Interface()) {
for _, vv := range val {
v.Add(key, vv)
}
}
} else { } else {
fieldValue := rValue.Field(i).String() fieldValue := rValue.Field(i).String()
v.Add(tag, fieldValue) v.Add(tag, fieldValue)

View file

@ -10,19 +10,23 @@
<div class="lg:flex-row flex py-2 lg:py-0 lg:ml-auto"> <div class="lg:flex-row flex py-2 lg:py-0 lg:ml-auto">
<form id="visibility" class="flex items-center whitespace-nowrap" method="post" action="{{ $.c.ExternalUrl }}/{{ .gist.User.Username }}/{{ .gist.Identifier }}/visibility"> <form id="visibility" class="flex items-center whitespace-nowrap" method="post" action="{{ $.c.ExternalUrl }}/{{ .gist.User.Username }}/{{ .gist.Identifier }}/visibility">
{{ .csrfHtml }} {{ .csrfHtml }}
<button type="submit" class="ml-auto relative inline-flex items-center space-x-2 rounded-md border border-gray-200 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 px-2 py-1.5 text-xs font-medium text-slate-700 dark:text-slate-300 hover:bg-gray-100 dark:hover:bg-gray-700 hover:border-gray-500 hover:text-slate-700 dark:hover:text-slate-300 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500 leading-3"> <div class="ml-auto inline-flex ">
{{ if eq .gist.Private 2 }} <button id="submit-gist" type="submit" name="private" value="0" class="ml-auto relative inline-flex items-center space-x-2 rounded-l-md border border-gray-200 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 px-2 py-1.5 text-xs font-medium text-slate-700 dark:text-slate-300 hover:bg-gray-100 dark:hover:bg-gray-700 hover:border-gray-500 hover:text-slate-700 dark:hover:text-slate-300 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500 leading-3">{{ .locale.Tr "gist.edit.change-visibility" }} {{ .locale.Tr "gist.public" }}</button>
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"> <div class="relative -ml-px block">
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /> <button type="button" class="ml-auto relative inline-flex items-center space-x-2 rounded-r-md border border-gray-200 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 px-2 py-1.5 text-xs font-medium text-slate-700 dark:text-slate-300 hover:bg-gray-100 dark:hover:bg-gray-700 hover:border-gray-500 hover:text-slate-700 dark:hover:text-slate-300 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500 leading-3" id="gist-visibility-menu-button">
<path stroke-linecap="round" stroke-linejoin="round" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" /> <svg class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
</svg> <path fill-rule="evenodd" d="M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z" clip-rule="evenodd" />
{{ else }} </svg>
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"> </button>
<path stroke-linecap="round" stroke-linejoin="round" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21" /> <div id="gist-menu-visibility" class="hidden absolute right-0 z-10 mt-2 origin-top-right rounded-md bg-white shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none" role="menu" aria-orientation="vertical" aria-labelledby="gist-visibility-menu-button">
</svg> <div class="rounded-md dark:bg-gray-800 bg-white shadow-lg ring-1 ring-gray-50 dark:ring-gray-700 focus:outline-none" role="none" style="word-break: keep-all">
{{ end }} <span class="text-gray-700 block px-4 py-2 text-sm cursor-pointer dark:text-slate-300 hover:text-slate-500 dark:hover:text-slate-400 gist-visibility-option" data-btntext="{{ .locale.Tr "gist.edit.change-visibility" }} {{ .locale.Tr "gist.public" }}" data-visibility="0" role="menuitem">{{ .locale.Tr "gist.public" }}</span>
{{ .locale.Tr "gist.edit.change-visibility" }} {{ visibilityStr .gist.Private.Next true }} <span class="text-gray-700 block px-4 py-2 text-sm cursor-pointer dark:text-slate-300 hover:text-slate-500 dark:hover:text-slate-400 gist-visibility-option" data-btntext="{{ .locale.Tr "gist.edit.change-visibility" }} {{ .locale.Tr "gist.unlisted" }}" data-visibility="1" role="menuitem">{{ .locale.Tr "gist.unlisted" }}</span>
</button> <span class="text-gray-700 block px-4 py-2 text-sm cursor-pointer dark:text-slate-300 hover:text-slate-500 dark:hover:text-slate-400 gist-visibility-option" data-btntext="{{ .locale.Tr "gist.edit.change-visibility" }} {{ .locale.Tr "gist.private" }}" data-visibility="2" role="menuitem">{{ .locale.Tr "gist.private" }}</span>
</div>
</div>
</div>
</div>
</form> </form>
<form id="delete" onsubmit="return confirm('Are you sure you want to delete this gist ?')" class="ml-2 flex items-center" method="post" action="{{ $.c.ExternalUrl }}/{{ .gist.User.Username }}/{{ .gist.Identifier }}/delete"> <form id="delete" onsubmit="return confirm('Are you sure you want to delete this gist ?')" class="ml-2 flex items-center" method="post" action="{{ $.c.ExternalUrl }}/{{ .gist.User.Username }}/{{ .gist.Identifier }}/delete">
{{ .csrfHtml }} {{ .csrfHtml }}