0
Fork 0
mirror of https://github.com/thomiceli/opengist.git synced 2025-04-02 02:51:25 -05:00

Add topics for Gists (#413)

This commit is contained in:
Thomas Miceli 2025-01-24 14:39:42 +01:00 committed by GitHub
parent 8369cbf2f0
commit f5b8881d35
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
25 changed files with 278 additions and 59 deletions

View file

@ -144,7 +144,7 @@ func Setup(dbUri string) error {
return err
}
if err = db.AutoMigrate(&User{}, &Gist{}, &SSHKey{}, &AdminSetting{}, &Invitation{}, &WebAuthnCredential{}, &TOTP{}); err != nil {
if err = db.AutoMigrate(&User{}, &Gist{}, &SSHKey{}, &AdminSetting{}, &Invitation{}, &WebAuthnCredential{}, &TOTP{}, &GistTopic{}); err != nil {
return err
}
@ -208,7 +208,7 @@ func setupSQLite(dbInfo databaseInfo) error {
u.Scheme = "file"
q := u.Query()
q.Set("_fk", "true")
q.Set("_pragma", "foreign_keys(1)")
q.Set("_journal_mode", journalMode)
u.RawQuery = q.Encode()
dsn = u.String()
@ -258,5 +258,5 @@ func DeprecationDBFilename() {
}
func TruncateDatabase() error {
return db.Migrator().DropTable("likes", &User{}, "gists", &SSHKey{}, &AdminSetting{}, &Invitation{}, &WebAuthnCredential{}, &TOTP{})
return db.Migrator().DropTable("likes", &User{}, "gists", &SSHKey{}, &AdminSetting{}, &Invitation{}, &WebAuthnCredential{}, &TOTP{}, &GistTopic{})
}

View file

@ -81,6 +81,8 @@ type Gist struct {
Likes []User `gorm:"many2many:likes;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
Forked *Gist `gorm:"foreignKey:ForkedID;constraint:OnUpdate:CASCADE,OnDelete:SET NULL"`
ForkedID uint
Topics []GistTopic `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
}
type Like struct {
@ -100,7 +102,7 @@ func (gist *Gist) BeforeDelete(tx *gorm.DB) error {
func GetGist(user string, gistUuid string) (*Gist, error) {
gist := new(Gist)
err := db.Preload("User").Preload("Forked.User").
err := db.Preload("User").Preload("Forked.User").Preload("Topics").
Where("(gists.uuid like ? OR gists.url = ?) AND users.username like ?", gistUuid+"%", gistUuid, user).
Joins("join users on gists.user_id = users.id").
First(&gist).Error
@ -110,7 +112,7 @@ func GetGist(user string, gistUuid string) (*Gist, error) {
func GetGistByID(gistId string) (*Gist, error) {
gist := new(Gist)
err := db.Preload("User").Preload("Forked.User").
err := db.Preload("User").Preload("Forked.User").Preload("Topics").
Where("gists.id = ?", gistId).
First(&gist).Error
@ -119,7 +121,9 @@ func GetGistByID(gistId string) (*Gist, error) {
func GetAllGistsForCurrentUser(currentUserId uint, offset int, sort string, order string) ([]*Gist, error) {
var gists []*Gist
err := db.Preload("User").Preload("Forked.User").
err := db.Preload("User").
Preload("Forked.User").
Preload("Topics").
Where("gists.private = 0 or gists.user_id = ?", currentUserId).
Limit(11).
Offset(offset * 10).
@ -140,12 +144,18 @@ func GetAllGists(offset int) ([]*Gist, error) {
return gists, err
}
func GetAllGistsFromSearch(currentUserId uint, query string, offset int, sort string, order string) ([]*Gist, error) {
func GetAllGistsFromSearch(currentUserId uint, query string, offset int, sort string, order string, topic string) ([]*Gist, error) {
var gists []*Gist
err := db.Preload("User").Preload("Forked.User").
tx := db.Preload("User").Preload("Forked.User").Preload("Topics").
Where("((gists.private = 0) or (gists.private > 0 and gists.user_id = ?))", currentUserId).
Where("gists.title like ? or gists.description like ?", "%"+query+"%", "%"+query+"%").
Limit(11).
Where("gists.title like ? or gists.description like ?", "%"+query+"%", "%"+query+"%")
if topic != "" {
tx = tx.Joins("join gist_topics on gists.id = gist_topics.gist_id").
Where("gist_topics.topic = ?", topic)
}
err := tx.Limit(11).
Offset(offset * 10).
Order("gists." + sort + "_at " + order).
Find(&gists).Error
@ -154,7 +164,7 @@ func GetAllGistsFromSearch(currentUserId uint, query string, offset int, sort st
}
func gistsFromUserStatement(fromUserId uint, currentUserId uint) *gorm.DB {
return db.Preload("User").Preload("Forked.User").
return db.Preload("User").Preload("Forked.User").Preload("Topics").
Where("((gists.private = 0) or (gists.private > 0 and gists.user_id = ?))", currentUserId).
Where("users.id = ?", fromUserId).
Joins("join users on gists.user_id = users.id")
@ -177,7 +187,7 @@ func CountAllGistsFromUser(fromUserId uint, currentUserId uint) (int64, error) {
}
func likedStatement(fromUserId uint, currentUserId uint) *gorm.DB {
return db.Preload("User").Preload("Forked.User").
return db.Preload("User").Preload("Forked.User").Preload("Topics").
Where("((gists.private = 0) or (gists.private > 0 and gists.user_id = ?))", currentUserId).
Where("likes.user_id = ?", fromUserId).
Joins("join likes on gists.id = likes.gist_id").
@ -200,7 +210,7 @@ func CountAllGistsLikedByUser(fromUserId uint, currentUserId uint) (int64, error
}
func forkedStatement(fromUserId uint, currentUserId uint) *gorm.DB {
return db.Preload("User").Preload("Forked.User").
return db.Preload("User").Preload("Forked.User").Preload("Topics").
Where("gists.forked_id is not null and ((gists.private = 0) or (gists.private > 0 and gists.user_id = ?))", currentUserId).
Where("gists.user_id = ?", fromUserId).
Joins("join users on gists.user_id = users.id")
@ -242,7 +252,7 @@ func GetAllGistsVisibleByUser(userId uint) ([]uint, error) {
func GetAllGistsByIds(ids []uint) ([]*Gist, error) {
var gists []*Gist
err := db.Preload("User").Preload("Forked.User").
err := db.Preload("User").Preload("Forked.User").Preload("Topics").
Where("id in ?", ids).
Find(&gists).Error
@ -259,6 +269,12 @@ func (gist *Gist) CreateForked() error {
}
func (gist *Gist) Update() error {
// reset the topics
err := db.Model(&GistTopic{}).Where("gist_id = ?", gist.ID).Delete(&GistTopic{}).Error
if err != nil {
return err
}
return db.Omit("forked_id").Save(&gist).Error
}
@ -535,6 +551,22 @@ func (gist *Gist) GetLanguagesFromFiles() ([]string, error) {
return languages, nil
}
func (gist *Gist) GetTopics() ([]string, error) {
var topics []string
err := db.Model(&GistTopic{}).
Where("gist_id = ?", gist.ID).
Pluck("topic", &topics).Error
return topics, err
}
func (gist *Gist) TopicsSlice() []string {
topics := make([]string, 0, len(gist.Topics))
for _, topic := range gist.Topics {
topics = append(topics, topic.Topic)
}
return topics
}
// -- DTO -- //
type GistDTO struct {
@ -544,6 +576,7 @@ type GistDTO struct {
Files []FileDTO `validate:"min=1,dive"`
Name []string `form:"name"`
Content []string `form:"content"`
Topics string `validate:"gisttopics" form:"topics"`
VisibilityDTO
}
@ -562,6 +595,7 @@ func (dto *GistDTO) ToGist() *Gist {
Description: dto.Description,
Private: dto.Private,
URL: dto.URL,
Topics: dto.TopicStrToSlice(),
}
}
@ -569,9 +603,19 @@ func (dto *GistDTO) ToExistingGist(gist *Gist) *Gist {
gist.Title = dto.Title
gist.Description = dto.Description
gist.URL = dto.URL
gist.Topics = dto.TopicStrToSlice()
return gist
}
func (dto *GistDTO) TopicStrToSlice() []GistTopic {
topics := strings.Fields(dto.Topics)
gistTopics := make([]GistTopic, 0, len(topics))
for _, topic := range topics {
gistTopics = append(gistTopics, GistTopic{Topic: topic})
}
return gistTopics
}
// -- Index -- //
func (gist *Gist) ToIndexedGist() (*index.Gist, error) {
@ -597,6 +641,11 @@ func (gist *Gist) ToIndexedGist() (*index.Gist, error) {
return nil, err
}
topics, err := gist.GetTopics()
if err != nil {
return nil, err
}
indexedGist := &index.Gist{
GistID: gist.ID,
Username: gist.User.Username,
@ -605,6 +654,7 @@ func (gist *Gist) ToIndexedGist() (*index.Gist, error) {
Filenames: fileNames,
Extensions: exts,
Languages: langs,
Topics: topics,
CreatedAt: gist.CreatedAt,
UpdatedAt: gist.UpdatedAt,
}

6
internal/db/gist_tag.go Normal file
View file

@ -0,0 +1,6 @@
package db
type GistTopic struct {
GistID uint `gorm:"primaryKey"`
Topic string `gorm:"primaryKey;size:50"`
}

View file

@ -45,6 +45,7 @@ gist.new.create-unlisted-button: Create unlisted gist
gist.new.create-private-button: Create private gist
gist.new.preview: Preview
gist.new.create-a-new-gist: Create a new gist
gist.new.topics: Topics (separate with spaces)
gist.edit.editing: Editing
gist.edit.edit-gist: Edit %s
@ -74,6 +75,9 @@ gist.list.no-gists: No gists
gist.list.all-liked-by: All gists liked by %s
gist.list.all-forked-by: All gists forked by %s
gist.list.all-from: All gists from %s
gist.list.topic-results-topic: All gists matching topic %s
gist.list.topic-results: All gists matching topic
gist.search.found: gists found
gist.search.no-results: No gists found
@ -82,6 +86,8 @@ gist.search.help.title: gists with given title
gist.search.help.filename: gists having files with given name
gist.search.help.extension: gists having files with given extension
gist.search.help.language: gists having files with given language
gist.search.help.topic: gists with given topic
gist.forks: Forks
gist.forks.view: View fork
@ -303,5 +309,6 @@ validation.should-only-contain-alphanumeric-characters: Field %s should only con
validation.should-only-contain-alphanumeric-characters-and-dashes: Field %s should only contain alphanumeric characters and dashes
validation.not-enough: Not enough %s
validation.invalid: Invalid %s
validation.invalid-gist-topics: Invalid gist topics, they must start with a letter or number, consist of 50 characters or less, and can include hyphens
html.title.admin-panel: Admin panel

View file

@ -170,6 +170,7 @@ func SearchGists(queryStr string, queryMetadata SearchGistMetadata, gistsIds []u
addQuery("Extensions", "."+queryMetadata.Extension)
addQuery("Filenames", queryMetadata.Filename)
addQuery("Languages", queryMetadata.Language)
addQuery("Topics", queryMetadata.Topic)
languageFacet := bleve.NewFacetRequest("Languages", 10)

View file

@ -8,6 +8,7 @@ type Gist struct {
Filenames []string
Extensions []string
Languages []string
Topics []string
CreatedAt int64
UpdatedAt int64
}
@ -18,4 +19,5 @@ type SearchGistMetadata struct {
Filename string
Extension string
Language string
Topic string
}

View file

@ -16,6 +16,7 @@ func NewValidator() *OpengistValidator {
_ = v.RegisterValidation("notreserved", validateReservedKeywords)
_ = v.RegisterValidation("alphanumdash", validateAlphaNumDash)
_ = v.RegisterValidation("alphanumdashorempty", validateAlphaNumDashOrEmpty)
_ = v.RegisterValidation("gisttopics", validateGistTopics)
return &OpengistValidator{v}
}
@ -46,6 +47,8 @@ func ValidationMessages(err *error, locale *i18n.Locale) string {
messages[i] = locale.String("validation.not-enough", e.Field())
case "notreserved":
messages[i] = locale.String("validation.invalid", e.Field())
case "gisttopics":
messages[i] = locale.String("validation.invalid-gist-topics")
}
}
@ -72,3 +75,27 @@ func validateAlphaNumDash(fl validator.FieldLevel) bool {
func validateAlphaNumDashOrEmpty(fl validator.FieldLevel) bool {
return regexp.MustCompile(`^$|^[a-zA-Z0-9-]+$`).MatchString(fl.Field().String())
}
func validateGistTopics(fl validator.FieldLevel) bool {
topicsInput := fl.Field().String()
if topicsInput == "" {
return true
}
topics := strings.Fields(topicsInput)
if len(topics) > 10 {
return false
}
for _, tag := range topics {
if len(tag) > 50 {
return false
}
if !regexp.MustCompile(`^[a-zA-Z0-9-]+$`).MatchString(tag) {
return false
}
}
return true
}

View file

@ -10,8 +10,6 @@ import (
"github.com/thomiceli/opengist/internal/web/handlers"
"gorm.io/gorm"
"html/template"
"regexp"
"strings"
)
func AllGists(ctx *context.Context) error {
@ -48,35 +46,25 @@ func AllGists(ctx *context.Context) error {
currentUserId = 0
}
mode := ctx.GetData("mode")
if fromUserStr == "" {
urlctx := ctx.Request().URL.Path
if strings.HasSuffix(urlctx, "search") {
if mode == "search" {
ctx.SetData("htmlTitle", ctx.TrH("gist.list.search-results"))
ctx.SetData("mode", "search")
ctx.SetData("searchQuery", ctx.QueryParam("q"))
ctx.SetData("searchQueryUrl", template.URL("&q="+ctx.QueryParam("q")))
urlPage = "search"
gists, err = db.GetAllGistsFromSearch(currentUserId, ctx.QueryParam("q"), pageInt-1, sort, order)
} else if strings.HasSuffix(urlctx, "all") {
gists, err = db.GetAllGistsFromSearch(currentUserId, ctx.QueryParam("q"), pageInt-1, sort, order, "")
} else if mode == "topics" {
ctx.SetData("htmlTitle", ctx.TrH("gist.list.topic-results-topic", ctx.Param("topic")))
ctx.SetData("topic", ctx.Param("topic"))
urlPage = "topics/" + ctx.Param("topic")
gists, err = db.GetAllGistsFromSearch(currentUserId, "", pageInt-1, sort, order, ctx.Param("topic"))
} else if mode == "all" {
ctx.SetData("htmlTitle", ctx.TrH("gist.list.all"))
ctx.SetData("mode", "all")
urlPage = "all"
gists, err = db.GetAllGistsForCurrentUser(currentUserId, pageInt-1, sort, order)
}
} else {
liked := false
forked := false
liked, err = regexp.MatchString(`/[^/]*/liked`, ctx.Request().URL.Path)
if err != nil {
return ctx.ErrorRes(500, "Error matching regexp", err)
}
forked, err = regexp.MatchString(`/[^/]*/forked`, ctx.Request().URL.Path)
if err != nil {
return ctx.ErrorRes(500, "Error matching regexp", err)
}
var fromUser *db.User
fromUser, err = db.GetUserByUsername(fromUserStr)
@ -106,20 +94,17 @@ func AllGists(ctx *context.Context) error {
ctx.SetData("countForked", countForked)
}
if liked {
if mode == "liked" {
urlPage = fromUserStr + "/liked"
ctx.SetData("htmlTitle", ctx.TrH("gist.list.all-liked-by", fromUserStr))
ctx.SetData("mode", "liked")
gists, err = db.GetAllGistsLikedByUser(fromUser.ID, currentUserId, pageInt-1, sort, order)
} else if forked {
} else if mode == "forked" {
urlPage = fromUserStr + "/forked"
ctx.SetData("htmlTitle", ctx.TrH("gist.list.all-forked-by", fromUserStr))
ctx.SetData("mode", "forked")
gists, err = db.GetAllGistsForkedByUser(fromUser.ID, currentUserId, pageInt-1, sort, order)
} else {
} else if mode == "fromUser" {
urlPage = fromUserStr
ctx.SetData("htmlTitle", ctx.TrH("gist.list.all-from", fromUserStr))
ctx.SetData("mode", "fromUser")
gists, err = db.GetAllGistsFromUser(fromUser.ID, currentUserId, pageInt-1, sort, order)
}
}
@ -171,6 +156,7 @@ func Search(ctx *context.Context) error {
Filename: meta["filename"],
Extension: meta["extension"],
Language: meta["language"],
Topic: meta["topic"],
}, visibleGistsIds, pageInt)
if err != nil {
return ctx.ErrorRes(500, "Error searching gists", err)

View file

@ -67,14 +67,14 @@ func ProcessCreate(ctx *context.Context) error {
if err != nil {
ctx.AddFlash(validator.ValidationMessages(&err, ctx.GetData("locale").(*i18n.Locale)), "error")
if isCreate {
return ctx.Html("create.html")
return ctx.HtmlWithCode(400, "create.html")
} else {
files, err := gist.Files("HEAD", false)
if err != nil {
return ctx.ErrorRes(500, "Error fetching files", err)
}
ctx.SetData("files", files)
return ctx.Html("edit.html")
return ctx.HtmlWithCode(400, "edit.html")
}
}

View file

@ -43,6 +43,7 @@ func Fork(ctx *context.Context) error {
UserID: currentUser.ID,
ForkedID: gist.ID,
NbFiles: gist.NbFiles,
Topics: gist.Topics,
}
if err = newGist.CreateForked(); err != nil {

View file

@ -54,6 +54,11 @@ func GistJson(ctx *context.Context) error {
renderedFiles := render.HighlightFiles(files)
ctx.SetData("files", renderedFiles)
topics, err := gist.GetTopics()
if err != nil {
return ctx.ErrorRes(500, "Error fetching topics for gist", err)
}
htmlbuf := bytes.Buffer{}
w := bufio.NewWriter(&htmlbuf)
if err = ctx.Echo().Renderer.Render(w, "gist_embed.html", ctx.DataMap(), ctx); err != nil {
@ -80,6 +85,7 @@ func GistJson(ctx *context.Context) error {
"created_at": time.Unix(gist.CreatedAt, 0).Format(time.RFC3339),
"visibility": gist.VisibilityStr(),
"files": renderedFiles,
"topics": topics,
"embed": map[string]string{
"html": htmlbuf.String(),
"css": cssUrl,

View file

@ -405,3 +405,11 @@ func gistNewPushSoftInit(next Handler) Handler {
return next(ctx)
}
}
func setAllGistsMode(mode string) Middleware {
return func(next Handler) Handler {
return func(ctx *context.Context) error {
ctx.SetData("mode", mode)
return next(ctx)
}
}
}

View file

@ -179,6 +179,16 @@ func (s *Server) setFuncMap() {
_, err := url.ParseRequestURI(s)
return err == nil
},
"topicsToStr": func(topics []db.GistTopic) string {
str := ""
for i, topic := range topics {
if i > 0 {
str += " "
}
str += topic.Topic
}
return str
},
}
t := template.Must(template.New("t").Funcs(fm).ParseFS(templates.Files, "*/*.html"))

View file

@ -90,17 +90,19 @@ func (s *Server) registerRoutes() {
r.Any("/init/*", git.GitHttp, gistNewPushSoftInit)
}
r.GET("/all", gist.AllGists, checkRequireLogin)
r.GET("/all", gist.AllGists, checkRequireLogin, setAllGistsMode("all"))
if index.Enabled() {
r.GET("/search", gist.Search, checkRequireLogin)
} else {
r.GET("/search", gist.AllGists, checkRequireLogin)
r.GET("/search", gist.AllGists, checkRequireLogin, setAllGistsMode("search"))
}
r.GET("/:user", gist.AllGists, checkRequireLogin)
r.GET("/:user/liked", gist.AllGists, checkRequireLogin)
r.GET("/:user/forked", gist.AllGists, checkRequireLogin)
r.GET("/:user", gist.AllGists, checkRequireLogin, setAllGistsMode("fromUser"))
r.GET("/:user/liked", gist.AllGists, checkRequireLogin, setAllGistsMode("liked"))
r.GET("/:user/forked", gist.AllGists, checkRequireLogin, setAllGistsMode("forked"))
r.GET("/topics/:topic", gist.AllGists, checkRequireLogin, setAllGistsMode("topics"))
sC := r.SubGroup("/:user/:gistname")
{

View file

@ -131,6 +131,7 @@ func TestAdminUser(t *testing.T) {
},
Name: []string{"gist1.txt"},
Content: []string{"yeah"},
Topics: "",
}
err := s.Request("POST", "/", gist1, 302)
require.NoError(t, err)
@ -170,6 +171,7 @@ func TestAdminGist(t *testing.T) {
},
Name: []string{"gist1.txt"},
Content: []string{"yeah"},
Topics: "",
}
err := s.Request("POST", "/", gist1, 302)
require.NoError(t, err)

View file

@ -110,6 +110,7 @@ func TestAnonymous(t *testing.T) {
},
Name: []string{"gist1.txt", "gist2.txt", "gist3.txt"},
Content: []string{"yeah", "yeah\ncool", "yeah\ncool gist actually"},
Topics: "",
}
err = s.Request("POST", "/", gist1, 302)
require.NoError(t, err)
@ -165,6 +166,7 @@ func TestGitOperations(t *testing.T) {
Content: []string{
"yeah",
},
Topics: "",
}
err := s.Request("POST", "/", gist1, 302)
require.NoError(t, err)
@ -180,6 +182,7 @@ func TestGitOperations(t *testing.T) {
Content: []string{
"cool",
},
Topics: "",
}
err = s.Request("POST", "/", gist2, 302)
require.NoError(t, err)
@ -195,6 +198,7 @@ func TestGitOperations(t *testing.T) {
Content: []string{
"super",
},
Topics: "",
}
err = s.Request("POST", "/", gist3, 302)
require.NoError(t, err)

View file

@ -21,7 +21,7 @@ func TestGists(t *testing.T) {
err = s.Request("GET", "/all", nil, 200)
require.NoError(t, err)
err = s.Request("POST", "/", nil, 200)
err = s.Request("POST", "/", nil, 400)
require.NoError(t, err)
gist1 := db.GistDTO{
@ -32,6 +32,7 @@ func TestGists(t *testing.T) {
},
Name: []string{"gist1.txt", "gist2.txt", "gist3.txt"},
Content: []string{"yeah", "yeah\ncool", "yeah\ncool gist actually"},
Topics: "",
}
err = s.Request("POST", "/", gist1, 302)
require.NoError(t, err)
@ -63,8 +64,9 @@ func TestGists(t *testing.T) {
},
Name: []string{"", "gist2.txt", "gist3.txt"},
Content: []string{"", "yeah\ncool", "yeah\ncool gist actually"},
Topics: "",
}
err = s.Request("POST", "/", gist2, 200)
err = s.Request("POST", "/", gist2, 400)
require.NoError(t, err)
gist3 := db.GistDTO{
@ -75,6 +77,7 @@ func TestGists(t *testing.T) {
},
Name: []string{""},
Content: []string{"yeah"},
Topics: "",
}
err = s.Request("POST", "/", gist3, 302)
require.NoError(t, err)
@ -86,7 +89,7 @@ func TestGists(t *testing.T) {
require.NoError(t, err)
require.Equal(t, "gistfile1.txt", gist3files[0])
err = s.Request("POST", "/"+gist1db.User.Username+"/"+gist1db.Uuid+"/edit", nil, 200)
err = s.Request("POST", "/"+gist1db.User.Username+"/"+gist1db.Uuid+"/edit", nil, 400)
require.NoError(t, err)
gist1.Name = []string{"gist1.txt"}
@ -118,6 +121,7 @@ func TestVisibility(t *testing.T) {
},
Name: []string{""},
Content: []string{"yeah"},
Topics: "",
}
err := s.Request("POST", "/", gist1, 302)
require.NoError(t, err)
@ -160,6 +164,7 @@ func TestLikeFork(t *testing.T) {
},
Name: []string{""},
Content: []string{"yeah"},
Topics: "",
}
err := s.Request("POST", "/", gist1, 302)
require.NoError(t, err)
@ -220,6 +225,7 @@ func TestCustomUrl(t *testing.T) {
},
Name: []string{"gist1.txt", "gist2.txt", "gist3.txt"},
Content: []string{"yeah", "yeah\ncool", "yeah\ncool gist actually"},
Topics: "",
}
err := s.Request("POST", "/", gist1, 302)
require.NoError(t, err)
@ -251,6 +257,7 @@ func TestCustomUrl(t *testing.T) {
},
Name: []string{"gist1.txt", "gist2.txt", "gist3.txt"},
Content: []string{"yeah", "yeah\ncool", "yeah\ncool gist actually"},
Topics: "",
}
err = s.Request("POST", "/", gist2, 302)
require.NoError(t, err)
@ -261,3 +268,75 @@ func TestCustomUrl(t *testing.T) {
require.Equal(t, gist2db.Uuid, gist2db.Identifier())
require.NotEqual(t, gist2db.URL, gist2db.Identifier())
}
func TestTopics(t *testing.T) {
s := Setup(t)
defer Teardown(t, s)
user1 := db.UserDTO{Username: "thomas", Password: "thomas"}
register(t, s, user1)
gist1 := db.GistDTO{
Title: "gist1",
URL: "my-gist",
Description: "my first gist",
VisibilityDTO: db.VisibilityDTO{
Private: 0,
},
Name: []string{"gist1.txt", "gist2.txt", "gist3.txt"},
Content: []string{"yeah", "yeah\ncool", "yeah\ncool gist actually"},
Topics: "topic1 topic2 topic3",
}
err := s.Request("POST", "/", gist1, 302)
require.NoError(t, err)
gist1db, err := db.GetGistByID("1")
require.NoError(t, err)
require.Equal(t, []db.GistTopic{
{GistID: 1, Topic: "topic1"},
{GistID: 1, Topic: "topic2"},
{GistID: 1, Topic: "topic3"},
}, gist1db.Topics)
gist2 := db.GistDTO{
Title: "gist2",
URL: "my-gist",
Description: "my second gist",
VisibilityDTO: db.VisibilityDTO{
Private: 0,
},
Name: []string{"gist1.txt", "gist2.txt", "gist3.txt"},
Content: []string{"yeah", "yeah\ncool", "yeah\ncool gist actually"},
Topics: "topic1 topic2 topic3 topic2 topic4 topic1",
}
err = s.Request("POST", "/", gist2, 302)
require.NoError(t, err)
gist2db, err := db.GetGistByID("2")
require.NoError(t, err)
require.Equal(t, []db.GistTopic{
{GistID: 2, Topic: "topic1"},
{GistID: 2, Topic: "topic2"},
{GistID: 2, Topic: "topic3"},
{GistID: 2, Topic: "topic4"},
}, gist2db.Topics)
gist3 := db.GistDTO{
Title: "gist3",
URL: "my-gist",
Description: "my third gist",
VisibilityDTO: db.VisibilityDTO{
Private: 0,
},
Name: []string{"gist1.txt", "gist2.txt", "gist3.txt"},
Content: []string{"yeah", "yeah\ncool", "yeah\ncool gist actually"},
Topics: "topic1 topic2 topic3 topic4 topic5 topic6 topic7 topic8 topic9 topic10 topic11",
}
err = s.Request("POST", "/", gist3, 400)
require.NoError(t, err)
gist3.Topics = "topictoolongggggggggggggggggggggggggggggggggggggggg"
err = s.Request("POST", "/", gist3, 400)
require.NoError(t, err)
}

View file

@ -32,9 +32,9 @@ module.exports = {
500: '#588fee',
600: '#3c79e2',
700: '#356fc0',
800: '#2d6195',
900: '#2a5574',
950: '#173040',
800: '#2b5da3',
900: '#1f4b8c',
950: '#192b57'
},
slate: colors.slate

View file

@ -34,9 +34,9 @@ module.exports = {
500: '#588fee',
600: '#3c79e2',
700: '#356fc0',
800: '#2d6195',
900: '#2a5574',
950: '#173040',
800: '#2b5da3',
900: '#1f4b8c',
950: '#192b57'
},
slate: colors.slate

View file

@ -114,6 +114,7 @@
<p class="text-gray-400"><code class="text-slate-800 dark:text-slate-300 pr-1">filename:myfile.txt</code> {{ .locale.Tr "gist.search.help.filename" }}</p>
<p class="text-gray-400"><code class="text-slate-800 dark:text-slate-300 pr-1">extension:yml</code> {{ .locale.Tr "gist.search.help.extension" }}</p>
<p class="text-gray-400"><code class="text-slate-800 dark:text-slate-300 pr-1">language:go</code> {{ .locale.Tr "gist.search.help.language" }}</p>
<p class="text-gray-400"><code class="text-slate-800 dark:text-slate-300 pr-1">topic:homelab</code> {{ .locale.Tr "gist.search.help.topic" }}</p>
</div>
</div>
</div>

View file

@ -95,6 +95,13 @@
{{ if .gist.Private }} • <span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-100 dark:bg-gray-700 text-slate-700 dark:text-slate-300"> {{ visibilityStr .gist.Private false }} </span>{{ end }}
</p>
<p class="mt-1 text-sm max-w-2xl text-slate-600 dark:text-slate-400">{{ .gist.Description }}</p>
{{ if .gist.Topics }}
<div class="mt-2">
{{ range .gist.Topics }}
<a href="{{ $.c.ExternalUrl }}/topics/{{ .Topic }}" class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-primary-200 text-primary-900 hover:bg-primary-300 dark:bg-primary-950 dark:text-primary-200 dark:hover:bg-primary-900">{{ .Topic }}</a>
{{ end }}
</div>
{{ end }}
</header>
<main class="mt-4">

View file

@ -18,6 +18,8 @@
<h1 class="text-2xl font-bold leading-tight">{{ .locale.Tr "gist.list.all" }}</h1>
{{ else if eq .mode "search" }}
<h1 class="text-2xl font-bold leading-tight">{{ .locale.Tr "gist.list.search-results" }}</h1>
{{ else if eq .mode "topics" }}
<h1 class="text-2xl font-bold leading-tight">{{ .locale.Tr "gist.list.topic-results" }} <span class="items-center px-2 py-0.5 rounded bg-primary-200 text-primary-900 hover:bg-primary-300 dark:bg-primary-950 dark:text-primary-200 dark:hover:bg-primary-900">{{ .topic }}</span></h1>
{{ end }}
{{ end }}
</div>
@ -58,7 +60,7 @@
</div>
</div>
{{ if and (ne .mode "all") (ne .mode "search") }}
{{ if and (ne .mode "all") (ne .mode "search") (ne .mode "topics") }}
<div class="mt-4">
<div class="sm:hidden">
<label for="tabs" class="sr-only">{{ .locale.Tr "gist.list.select-tab" }}</label>

View file

@ -25,6 +25,10 @@
<div class="col-span-6 sm:col-span-3 mt-2">
<input type="text" placeholder="{{ .locale.Tr "gist.new.url" }}" name="url" id="url" class="bg-white dark:bg-black shadow-sm focus:ring-primary-500 focus:border-primary-500 block w-full sm:text-sm border-gray-200 dark:border-gray-700 rounded-md" maxlength="32">
</div>
<div class="col-span-12 sm:col-span-9 mt-2">
<input type="text" placeholder="{{ .locale.Tr "gist.new.topics" }}" name="topics" id="topics" class="bg-white dark:bg-black shadow-sm focus:ring-primary-500 focus:border-primary-500 block w-full sm:text-sm border-gray-200 dark:border-gray-700 rounded-md">
</div>
</div>
</div>
<div id="editors" class="space-y-4">

View file

@ -58,6 +58,9 @@
<div class="col-span-6 sm:col-span-3 mt-2">
<input type="text" value="{{ .gist.URL }}" placeholder="{{ .locale.Tr "gist.new.url" }}" name="url" id="url" class="bg-white dark:bg-black shadow-sm focus:ring-primary-500 focus:border-primary-500 block w-full sm:text-sm border-gray-200 dark:border-gray-700 rounded-md" maxlength="32">
</div>
<div class="col-span-12 sm:col-span-9 mt-2">
<input type="text" value="{{ topicsToStr .gist.Topics }}" placeholder="{{ .locale.Tr "gist.new.topics" }}" name="topics" id="topics" class="bg-white dark:bg-black shadow-sm focus:ring-primary-500 focus:border-primary-500 block w-full sm:text-sm border-gray-200 dark:border-gray-700 rounded-md">
</div>
</div>
</div>
<div id="editors" class="space-y-4">

View file

@ -38,7 +38,18 @@
<h5 class="text-sm text-slate-500 pb-1">{{ .locale.Tr "gist.list.last-active" }} <span class="moment-timestamp">{{ .gist.UpdatedAt }}</span>
{{ if .gist.Forked }} • {{ .locale.Tr "gist.list.forked-from" }} <a href="{{ .c.ExternalUrl }}/{{ .gist.Forked.User.Username }}/{{ .gist.Forked.Identifier }}">{{ .gist.Forked.User.Username }}/{{ .gist.Forked.Title }}</a> {{ end }}
{{ if .gist.Private }} • <span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-100 dark:bg-gray-700 text-slate-700 dark:text-slate-300"> {{ visibilityStr .gist.Private false }} </span>{{ end }}</h5>
<h6 class="text-xs text-slate-700 dark:text-slate-300 py-1">{{ .gist.Description }}</h6>
<div class="flex items-center gap-2 mb-2">
{{ if len .gist.Description }}
<h6 class="text-xs text-slate-700 dark:text-slate-300">{{ .gist.Description }}</h6>
{{ end }}
{{ if len .gist.TopicsSlice }}
<div class="flex flex-wrap items-center gap-1">
{{ range .gist.TopicsSlice }}
<a href="{{ $.c.ExternalUrl }}/topics/{{ . }}" class="inline-flex items-center px-2 py-0.5 rounded text-xs bg-primary-200 text-primary-900 hover:bg-primary-300 dark:bg-primary-950 dark:text-primary-200 dark:hover:bg-primary-900">{{ . }}</a>
{{ end }}
</div>
{{ end }}
</div>
</div>
</div>
<a href="{{ .c.ExternalUrl }}/{{ .gist.User.Username }}/{{ .gist.Identifier }}" class="text-slate-700 dark:text-slate-300">