You can now re-order forums by dragging them in the Forum Manager.

Added some visual and textual hints to make it clearer that Menu Items and Forums can be dragged.
Added a hint to flush the page after pushing the <head>
Added the notice client template and pushNotice client function.

Used a pointer instead of a struct for AnalyticsTimeRange in the analytics routes.
Caught a potential missing error check in InitPhrases.
Use struct{} instead of bool in some of the user mapping maps for WebSockets to save space.
Added the buildUserExprs function to eliminate a bit of duplication.
Fixed a typo in ForumsEdit where it referenced a non-existent notice phrase.
Client hooks can now sort of return things.
Panel phrases are now fetched by init.js, but only in the control panel.
Reduced the number of unused phrases loaded in both the front-end and the control panel.

Plugin hyperdrive should handle Gzip better now.

Added the panel.ForumsOrderSubmit route.

Added the panel_hints_reorder phrase.
Moved the panel_forums phrases into the panel. namespace.
Added the panel.forums_order_updated phrase.
Renamed the panel_themes_menus_item_edit_button_aria phrase to panel_themes_menus_items_edit_button_aria
Renamed the panel_themes_menus_item_delete_button_aria phrase to panel_themes_menus_items_delete_button_aria
Added the panel_themes_menus_items_update_button phrase.

You will need to run the patcher / updater for this commit.
This commit is contained in:
Azareal 2019-04-27 16:32:26 +10:00
parent 2964cd767d
commit 27a4a74840
31 changed files with 798 additions and 511 deletions

View File

@ -182,6 +182,7 @@ func createTables(adapter qgen.Adapter) error {
tblColumn{"name", "varchar", 100, false, false, ""}, tblColumn{"name", "varchar", 100, false, false, ""},
tblColumn{"desc", "varchar", 200, false, false, ""}, tblColumn{"desc", "varchar", 200, false, false, ""},
tblColumn{"active", "boolean", 0, false, false, "1"}, tblColumn{"active", "boolean", 0, false, false, "1"},
tblColumn{"order", "int", 0, false, false, "0"},
tblColumn{"topicCount", "int", 0, false, false, "0"}, tblColumn{"topicCount", "int", 0, false, false, "0"},
tblColumn{"preset", "varchar", 100, false, false, "''"}, tblColumn{"preset", "varchar", 100, false, false, "''"},
tblColumn{"parentID", "int", 0, false, false, "0"}, tblColumn{"parentID", "int", 0, false, false, "0"},
@ -427,6 +428,7 @@ func createTables(adapter qgen.Adapter) error {
[]tblColumn{ []tblColumn{
tblColumn{"uname", "varchar", 180, false, false, ""}, tblColumn{"uname", "varchar", 180, false, false, ""},
tblColumn{"default", "boolean", 0, false, false, "0"}, tblColumn{"default", "boolean", 0, false, false, "0"},
//tblColumn{"profileUserVars", "text", 0, false, false, "''"},
}, },
[]tblKey{ []tblKey{
tblKey{"uname", "unique"}, tblKey{"uname", "unique"},

View File

@ -1,7 +1,7 @@
package common package common
//import "fmt"
import ( import (
//"log"
"database/sql" "database/sql"
"errors" "errors"
"strconv" "strconv"
@ -28,6 +28,7 @@ type Forum struct {
Name string Name string
Desc string Desc string
Active bool Active bool
Order int
Preset string Preset string
ParentID int ParentID int
ParentType string ParentType string
@ -135,8 +136,22 @@ func (sf SortForum) Len() int {
func (sf SortForum) Swap(i, j int) { func (sf SortForum) Swap(i, j int) {
sf[i], sf[j] = sf[j], sf[i] sf[i], sf[j] = sf[j], sf[i]
} }
/*func (sf SortForum) Less(i,j int) bool {
l := sf.less(i,j)
if l {
log.Printf("%s is less than %s. order: %d. id: %d.",sf[i].Name, sf[j].Name, sf[i].Order, sf[i].ID)
} else {
log.Printf("%s is not less than %s. order: %d. id: %d.",sf[i].Name, sf[j].Name, sf[i].Order, sf[i].ID)
}
return l
}*/
func (sf SortForum) Less(i, j int) bool { func (sf SortForum) Less(i, j int) bool {
return sf[i].ID < sf[j].ID if sf[i].Order < sf[j].Order {
return true
} else if sf[i].Order == sf[j].Order {
return sf[i].ID < sf[j].ID
}
return false
} }
// ! Don't use this outside of tests and possibly template_init.go // ! Don't use this outside of tests and possibly template_init.go

View File

@ -42,6 +42,7 @@ type ForumStore interface {
//GetChildren(parentID int, parentType string) ([]*Forum,error) //GetChildren(parentID int, parentType string) ([]*Forum,error)
//GetFirstChild(parentID int, parentType string) (*Forum,error) //GetFirstChild(parentID int, parentType string) (*Forum,error)
Create(forumName string, forumDesc string, active bool, preset string) (int, error) Create(forumName string, forumDesc string, active bool, preset string) (int, error)
UpdateOrder(updateMap map[int]int) error
GlobalCount() int GlobalCount() int
} }
@ -66,6 +67,7 @@ type MemoryForumStore struct {
updateCache *sql.Stmt updateCache *sql.Stmt
addTopics *sql.Stmt addTopics *sql.Stmt
removeTopics *sql.Stmt removeTopics *sql.Stmt
updateOrder *sql.Stmt
} }
// NewMemoryForumStore gives you a new instance of MemoryForumStore // NewMemoryForumStore gives you a new instance of MemoryForumStore
@ -73,17 +75,19 @@ func NewMemoryForumStore() (*MemoryForumStore, error) {
acc := qgen.NewAcc() acc := qgen.NewAcc()
// TODO: Do a proper delete // TODO: Do a proper delete
return &MemoryForumStore{ return &MemoryForumStore{
get: acc.Select("forums").Columns("name, desc, active, preset, parentID, parentType, topicCount, lastTopicID, lastReplyerID").Where("fid = ?").Prepare(), get: acc.Select("forums").Columns("name, desc, active, order, preset, parentID, parentType, topicCount, lastTopicID, lastReplyerID").Where("fid = ?").Prepare(),
getAll: acc.Select("forums").Columns("fid, name, desc, active, preset, parentID, parentType, topicCount, lastTopicID, lastReplyerID").Orderby("fid ASC").Prepare(), getAll: acc.Select("forums").Columns("fid, name, desc, active, order, preset, parentID, parentType, topicCount, lastTopicID, lastReplyerID").Orderby("order ASC, fid ASC").Prepare(),
delete: acc.Update("forums").Set("name= '', active = 0").Where("fid = ?").Prepare(), delete: acc.Update("forums").Set("name= '', active = 0").Where("fid = ?").Prepare(),
create: acc.Insert("forums").Columns("name, desc, active, preset").Fields("?,?,?,?").Prepare(), create: acc.Insert("forums").Columns("name, desc, active, preset").Fields("?,?,?,?").Prepare(),
count: acc.Count("forums").Where("name != ''").Prepare(), count: acc.Count("forums").Where("name != ''").Prepare(),
updateCache: acc.Update("forums").Set("lastTopicID = ?, lastReplyerID = ?").Where("fid = ?").Prepare(), updateCache: acc.Update("forums").Set("lastTopicID = ?, lastReplyerID = ?").Where("fid = ?").Prepare(),
addTopics: acc.Update("forums").Set("topicCount = topicCount + ?").Where("fid = ?").Prepare(), addTopics: acc.Update("forums").Set("topicCount = topicCount + ?").Where("fid = ?").Prepare(),
removeTopics: acc.Update("forums").Set("topicCount = topicCount - ?").Where("fid = ?").Prepare(), removeTopics: acc.Update("forums").Set("topicCount = topicCount - ?").Where("fid = ?").Prepare(),
updateOrder: acc.Update("forums").Set("order = ?").Where("fid = ?").Prepare(),
}, acc.FirstError() }, acc.FirstError()
} }
// TODO: Rename to ReloadAll?
// TODO: Add support for subforums // TODO: Add support for subforums
func (mfs *MemoryForumStore) LoadForums() error { func (mfs *MemoryForumStore) LoadForums() error {
var forumView []*Forum var forumView []*Forum
@ -103,7 +107,7 @@ func (mfs *MemoryForumStore) LoadForums() error {
var i = 0 var i = 0
for ; rows.Next(); i++ { for ; rows.Next(); i++ {
forum := &Forum{ID: 0, Active: true, Preset: "all"} forum := &Forum{ID: 0, Active: true, Preset: "all"}
err = rows.Scan(&forum.ID, &forum.Name, &forum.Desc, &forum.Active, &forum.Preset, &forum.ParentID, &forum.ParentType, &forum.TopicCount, &forum.LastTopicID, &forum.LastReplyerID) err = rows.Scan(&forum.ID, &forum.Name, &forum.Desc, &forum.Active, &forum.Order, &forum.Preset, &forum.ParentID, &forum.ParentType, &forum.TopicCount, &forum.LastTopicID, &forum.LastReplyerID)
if err != nil { if err != nil {
return err return err
} }
@ -161,7 +165,7 @@ func (mfs *MemoryForumStore) Get(id int) (*Forum, error) {
fint, ok := mfs.forums.Load(id) fint, ok := mfs.forums.Load(id)
if !ok || fint.(*Forum).Name == "" { if !ok || fint.(*Forum).Name == "" {
var forum = &Forum{ID: id} var forum = &Forum{ID: id}
err := mfs.get.QueryRow(id).Scan(&forum.Name, &forum.Desc, &forum.Active, &forum.Preset, &forum.ParentID, &forum.ParentType, &forum.TopicCount, &forum.LastTopicID, &forum.LastReplyerID) err := mfs.get.QueryRow(id).Scan(&forum.Name, &forum.Desc, &forum.Active, &forum.Order, &forum.Preset, &forum.ParentID, &forum.ParentType, &forum.TopicCount, &forum.LastTopicID, &forum.LastReplyerID)
if err != nil { if err != nil {
return forum, err return forum, err
} }
@ -178,7 +182,7 @@ func (mfs *MemoryForumStore) Get(id int) (*Forum, error) {
func (mfs *MemoryForumStore) BypassGet(id int) (*Forum, error) { func (mfs *MemoryForumStore) BypassGet(id int) (*Forum, error) {
var forum = &Forum{ID: id} var forum = &Forum{ID: id}
err := mfs.get.QueryRow(id).Scan(&forum.Name, &forum.Desc, &forum.Active, &forum.Preset, &forum.ParentID, &forum.ParentType, &forum.TopicCount, &forum.LastTopicID, &forum.LastReplyerID) err := mfs.get.QueryRow(id).Scan(&forum.Name, &forum.Desc, &forum.Active, &forum.Order, &forum.Preset, &forum.ParentID, &forum.ParentType, &forum.TopicCount, &forum.LastTopicID, &forum.LastReplyerID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -206,7 +210,7 @@ func (mfs *MemoryForumStore) BulkGetCopy(ids []int) (forums []Forum, err error)
func (mfs *MemoryForumStore) Reload(id int) error { func (mfs *MemoryForumStore) Reload(id int) error {
var forum = &Forum{ID: id} var forum = &Forum{ID: id}
err := mfs.get.QueryRow(id).Scan(&forum.Name, &forum.Desc, &forum.Active, &forum.Preset, &forum.ParentID, &forum.ParentType, &forum.TopicCount, &forum.LastTopicID, &forum.LastReplyerID) err := mfs.get.QueryRow(id).Scan(&forum.Name, &forum.Desc, &forum.Active, &forum.Order, &forum.Preset, &forum.ParentID, &forum.ParentType, &forum.TopicCount, &forum.LastTopicID, &forum.LastReplyerID)
if err != nil { if err != nil {
return err return err
} }
@ -348,6 +352,17 @@ func (mfs *MemoryForumStore) Create(forumName string, forumDesc string, active b
return fid, nil return fid, nil
} }
// TODO: Make this atomic, maybe with a transaction?
func (s *MemoryForumStore) UpdateOrder(updateMap map[int]int) error {
for fid, order := range updateMap {
_, err := s.updateOrder.Exec(order, fid)
if err != nil {
return err
}
}
return s.LoadForums()
}
// ! Might be slightly inaccurate, if the sync.Map is constantly shifting and churning, but it'll stabilise eventually. Also, slow. Don't use this on every request x.x // ! Might be slightly inaccurate, if the sync.Map is constantly shifting and churning, but it'll stabilise eventually. Also, slow. Don't use this on every request x.x
// Length returns the number of forums in the memory cache // Length returns the number of forums in the memory cache
func (mfs *MemoryForumStore) Length() (length int) { func (mfs *MemoryForumStore) Length() (length int) {

View File

@ -39,6 +39,7 @@ type LevelPhrases struct {
type LanguagePack struct { type LanguagePack struct {
Name string Name string
IsoCode string IsoCode string
//LastUpdated string
// Should we use a sync map or a struct for these? It would be nice, if we could keep all the phrases consistent. // Should we use a sync map or a struct for these? It would be nice, if we could keep all the phrases consistent.
Levels LevelPhrases Levels LevelPhrases
@ -70,6 +71,9 @@ func InitPhrases(lang string) error {
if f.IsDir() { if f.IsDir() {
return nil return nil
} }
if err != nil {
return err
}
data, err := ioutil.ReadFile(path) data, err := ioutil.ReadFile(path)
if err != nil { if err != nil {

View File

@ -158,6 +158,7 @@ func panelUserCheck(w http.ResponseWriter, r *http.Request, user *User) (header
header.AddPreScriptAsync("template_" + name + tname + ".js") header.AddPreScriptAsync("template_" + name + tname + ".js")
} }
addPreScript("alert") addPreScript("alert")
addPreScript("notice")
return header, stats, nil return header, stats, nil
} }
@ -267,6 +268,7 @@ func PrepResources(user *User, header *Header, theme *Theme) {
addPreScript("topics_topic") addPreScript("topics_topic")
addPreScript("paginator") addPreScript("paginator")
addPreScript("alert") addPreScript("alert")
addPreScript("notice")
if user.Loggedin { if user.Loggedin {
addPreScript("topic_c_edit_post") addPreScript("topic_c_edit_post")
addPreScript("topic_c_attach_item") addPreScript("topic_c_attach_item")

View File

@ -481,6 +481,8 @@ func compileJSTemplates(wg *sync.WaitGroup, c *tmpl.CTemplateSet, themeName stri
tmpls.AddStd("topic_c_attach_item", "common.TopicCAttachItem", TopicCAttachItem{ID: 1, ImgSrc: "", Path: "", FullPath: ""}) tmpls.AddStd("topic_c_attach_item", "common.TopicCAttachItem", TopicCAttachItem{ID: 1, ImgSrc: "", Path: "", FullPath: ""})
tmpls.AddStd("notice", "string", "nonono")
var dirPrefix = "./tmpl_client/" var dirPrefix = "./tmpl_client/"
var writeTemplate = func(name string, content string) { var writeTemplate = func(name string, content string) {
log.Print("Writing template '" + name + "'") log.Print("Writing template '" + name + "'")
@ -711,6 +713,10 @@ func initDefaultTmplFuncMap() {
return "" return ""
} }
fmap["flush"] = func() interface{} {
return nil
}
DefaultTemplateFuncMap = fmap DefaultTemplateFuncMap = fmap
} }

View File

@ -74,6 +74,7 @@ type CTemplateSet struct {
logger *log.Logger logger *log.Logger
loggerf *os.File loggerf *os.File
lang string
} }
func NewCTemplateSet(in string) *CTemplateSet { func NewCTemplateSet(in string) *CTemplateSet {
@ -112,9 +113,11 @@ func NewCTemplateSet(in string) *CTemplateSet {
"scope": true, "scope": true,
"dyntmpl": true, "dyntmpl": true,
"index": true, "index": true,
"flush": true,
}, },
logger: log.New(f, "", log.LstdFlags), logger: log.New(f, "", log.LstdFlags),
loggerf: f, loggerf: f,
lang:in,
} }
} }
@ -445,6 +448,16 @@ func (c *CTemplateSet) compile(name string, content string, expects string, expe
return errors.New("invalid page struct value") return errors.New("invalid page struct value")
} }
` `
if c.lang == "normal" {
fout += `var iw http.ResponseWriter
gzw, ok := w.(common.GzipResponseWriter)
if ok {
iw = gzw.ResponseWriter
}
_ = iw
`
}
if len(c.langIndexToName) > 0 { if len(c.langIndexToName) > 0 {
fout += "var plist = phrases.GetTmplPhrasesBytes(" + fname + "_tmpl_phrase_id)\n" fout += "var plist = phrases.GetTmplPhrasesBytes(" + fname + "_tmpl_phrase_id)\n"
} }
@ -587,16 +600,7 @@ func (c *CTemplateSet) compileSwitch(con CContext, node parse.Node) {
c.detail("Expression:", expr) c.detail("Expression:", expr)
// Simple member / guest optimisation for now // Simple member / guest optimisation for now
// TODO: Expand upon this // TODO: Expand upon this
var userExprs = []string{ userExprs, negUserExprs := buildUserExprs(con.RootHolder)
con.RootHolder + ".CurrentUser.Loggedin",
con.RootHolder + ".CurrentUser.IsSuperMod",
con.RootHolder + ".CurrentUser.IsAdmin",
}
var negUserExprs = []string{
"!" + con.RootHolder + ".CurrentUser.Loggedin",
"!" + con.RootHolder + ".CurrentUser.IsSuperMod",
"!" + con.RootHolder + ".CurrentUser.IsAdmin",
}
if c.guestOnly { if c.guestOnly {
c.detail("optimising away member branch") c.detail("optimising away member branch")
if inSlice(userExprs, expr) { if inSlice(userExprs, expr) {
@ -1170,6 +1174,16 @@ ArgLoop:
out += "if err != nil {\nreturn err\n}\n}\n" out += "if err != nil {\nreturn err\n}\n}\n"
literal = true literal = true
break ArgLoop break ArgLoop
case "flush":
if c.lang == "js" {
continue
}
out = "if fl, ok := iw.(http.Flusher); ok {\n"
out += "fl.Flush()\n"
out += "}\n"
literal = true
c.importMap["net/http"] = "net/http"
break ArgLoop
default: default:
c.detail("Variable!") c.detail("Variable!")
if len(node.Args) > (pos + 1) { if len(node.Args) > (pos + 1) {
@ -1391,6 +1405,20 @@ func (c *CTemplateSet) retCall(name string, params ...interface{}) {
c.detail("returned from " + name + " => (" + pstr + ")") c.detail("returned from " + name + " => (" + pstr + ")")
} }
func buildUserExprs(holder string) ([]string,[]string) {
var userExprs = []string{
holder + ".CurrentUser.Loggedin",
holder + ".CurrentUser.IsSuperMod",
holder + ".CurrentUser.IsAdmin",
}
var negUserExprs = []string{
"!" + holder + ".CurrentUser.Loggedin",
"!" + holder + ".CurrentUser.IsSuperMod",
"!" + holder + ".CurrentUser.IsAdmin",
}
return userExprs, negUserExprs
}
func (c *CTemplateSet) compileVarSub(con CContext, varname string, val reflect.Value, assLines string, onEnd func(string) string) { func (c *CTemplateSet) compileVarSub(con CContext, varname string, val reflect.Value, assLines string, onEnd func(string) string) {
c.dumpCall("compileVarSub", con, varname, val, assLines, onEnd) c.dumpCall("compileVarSub", con, varname, val, assLines, onEnd)
defer c.retCall("compileVarSub") defer c.retCall("compileVarSub")
@ -1438,16 +1466,7 @@ func (c *CTemplateSet) compileVarSub(con CContext, varname string, val reflect.V
// TODO: Take c.memberOnly into account // TODO: Take c.memberOnly into account
// TODO: Make this a template fragment so more optimisations can be applied to this // TODO: Make this a template fragment so more optimisations can be applied to this
// TODO: De-duplicate this logic // TODO: De-duplicate this logic
var userExprs = []string{ userExprs, negUserExprs := buildUserExprs(con.RootHolder)
con.RootHolder + ".CurrentUser.Loggedin",
con.RootHolder + ".CurrentUser.IsSuperMod",
con.RootHolder + ".CurrentUser.IsAdmin",
}
var negUserExprs = []string{
"!" + con.RootHolder + ".CurrentUser.Loggedin",
"!" + con.RootHolder + ".CurrentUser.IsSuperMod",
"!" + con.RootHolder + ".CurrentUser.IsAdmin",
}
if c.guestOnly { if c.guestOnly {
c.detail("optimising away member branch") c.detail("optimising away member branch")
if inSlice(userExprs, varname) { if inSlice(userExprs, varname) {
@ -1804,4 +1823,4 @@ func (c *CTemplateSet) error(args ...interface{}) {
func (c *CTemplateSet) critical(args ...interface{}) { func (c *CTemplateSet) critical(args ...interface{}) {
c.logger.Println(args...) c.logger.Println(args...)
} }

View File

@ -33,8 +33,8 @@ var errWsNouser = errors.New("This user isn't connected via WebSockets")
func init() { func init() {
adminStatsWatchers = make(map[*websocket.Conn]*WSUser) adminStatsWatchers = make(map[*websocket.Conn]*WSUser)
topicListWatchers = make(map[*WSUser]bool) topicListWatchers = make(map[*WSUser]struct{})
topicWatchers = make(map[int]map[*WSUser]bool) topicWatchers = make(map[int]map[*WSUser]struct{})
} }
//easyjson:json //easyjson:json
@ -130,7 +130,7 @@ func wsPageResponses(wsUser *WSUser, conn *websocket.Conn, page string) {
// TODO: Optimise this to reduce the amount of contention // TODO: Optimise this to reduce the amount of contention
case page == "/topics/": case page == "/topics/":
topicListMutex.Lock() topicListMutex.Lock()
topicListWatchers[wsUser] = true topicListWatchers[wsUser] = struct{}{}
topicListMutex.Unlock() topicListMutex.Unlock()
// TODO: Evict from page when permissions change? Or check user perms every-time before sending data? // TODO: Evict from page when permissions change? Or check user perms every-time before sending data?
case strings.HasPrefix(page, "/topic/"): case strings.HasPrefix(page, "/topic/"):
@ -169,9 +169,9 @@ func wsPageResponses(wsUser *WSUser, conn *websocket.Conn, page string) {
topicMutex.Lock() topicMutex.Lock()
_, ok := topicWatchers[topic.ID] _, ok := topicWatchers[topic.ID]
if !ok { if !ok {
topicWatchers[topic.ID] = make(map[*WSUser]bool) topicWatchers[topic.ID] = make(map[*WSUser]struct{})
} }
topicWatchers[topic.ID][wsUser] = true topicWatchers[topic.ID][wsUser] = struct{}{}
topicMutex.Unlock() topicMutex.Unlock()
case page == "/panel/": case page == "/panel/":
if !wsUser.User.IsSuperMod { if !wsUser.User.IsSuperMod {
@ -243,9 +243,9 @@ func wsLeavePage(wsUser *WSUser, conn *websocket.Conn, page string) {
// TODO: Abstract this // TODO: Abstract this
// TODO: Use odd-even sharding // TODO: Use odd-even sharding
var topicListWatchers map[*WSUser]bool var topicListWatchers map[*WSUser]struct{}
var topicListMutex sync.RWMutex var topicListMutex sync.RWMutex
var topicWatchers map[int]map[*WSUser]bool // map[tid]watchers var topicWatchers map[int]map[*WSUser]struct{} // map[tid]watchers
var topicMutex sync.RWMutex var topicMutex sync.RWMutex
var adminStatsWatchers map[*websocket.Conn]*WSUser var adminStatsWatchers map[*websocket.Conn]*WSUser
var adminStatsMutex sync.RWMutex var adminStatsMutex sync.RWMutex

View File

@ -35,6 +35,7 @@ func deactivateHdrive(plugin *c.Plugin) {
type Hyperspace struct { type Hyperspace struct {
topicList atomic.Value topicList atomic.Value
gzipTopicList atomic.Value
} }
func newHyperspace() *Hyperspace { func newHyperspace() *Hyperspace {
@ -48,10 +49,7 @@ func tickHdriveWol(args ...interface{}) (skip bool, rerr c.RouteError) {
return tickHdrive(args) return tickHdrive(args)
} }
// TODO: Find a better way of doing this func dummyReqHdrive() http.ResponseWriter {
func tickHdrive(args ...interface{}) (skip bool, rerr c.RouteError) {
c.DebugLog("Refueling...")
w := httptest.NewRecorder()
req := httptest.NewRequest("get", "/topics/", bytes.NewReader(nil)) req := httptest.NewRequest("get", "/topics/", bytes.NewReader(nil))
user := c.GuestUser user := c.GuestUser
@ -68,17 +66,48 @@ func tickHdrive(args ...interface{}) (skip bool, rerr c.RouteError) {
} }
if w.Code != 200 { if w.Code != 200 {
c.LogWarning(err) c.LogWarning(err)
return false, nil
} }
return w
}
// TODO: Find a better way of doing this
func tickHdrive(args ...interface{}) (skip bool, rerr c.RouteError) {
c.DebugLog("Refueling...")
w := httptest.NewRecorder()
dummyReqHdrive(w)
buf := new(bytes.Buffer) buf := new(bytes.Buffer)
buf.ReadFrom(w.Result().Body) buf.ReadFrom(w.Result().Body)
hyperspace.topicList.Store(buf.Bytes()) hyperspace.topicList.Store(buf.Bytes())
w = httptest.NewRecorder()
w.Header().Set("Content-Encoding", "gzip")
w.Header().Set("Content-Type", "text/html; charset=utf-8")
gz := gzip.NewWriter(w)
w = c.GzipResponseWriter{Writer: gz, ResponseWriter: w}
dummyReqHdrive(w)
buf = new(bytes.Buffer)
buf.ReadFrom(w.Result().Body)
hyperspace.gzipTopicList.Store(buf.Bytes())
if w.Header().Get("Content-Encoding") == "gzip" {
gz.Close()
}
return false, nil return false, nil
} }
func jumpHdrive(args ...interface{}) (skip bool, rerr c.RouteError) { func jumpHdrive(args ...interface{}) (skip bool, rerr c.RouteError) {
tList := hyperspace.topicList.Load().([]byte) var tList []byte
w := args[0].(http.ResponseWriter)
_, ok := w.(c.GzipResponseWriter)
if ok {
tList = hyperspace.gzipTopicList.Load().([]byte)
} else {
tList = hyperspace.topicList.Load().([]byte)
}
if len(tList) == 0 { if len(tList) == 0 {
c.DebugLog("no topiclist in hyperspace") c.DebugLog("no topiclist in hyperspace")
return false, nil return false, nil
@ -101,7 +130,6 @@ func jumpHdrive(args ...interface{}) (skip bool, rerr c.RouteError) {
//c.DebugLog //c.DebugLog
c.DebugLog("Successful jump") c.DebugLog("Successful jump")
w := args[0].(http.ResponseWriter)
header := args[3].(*c.Header) header := args[3].(*c.Header)
routes.FootHeaders(w, header) routes.FootHeaders(w, header)
w.Write(tList) w.Write(tList)

File diff suppressed because it is too large Load Diff

View File

@ -715,6 +715,8 @@
"option_yes":"Yes", "option_yes":"Yes",
"option_no":"No", "option_no":"No",
"panel_hints_reorder":"Drag to change the order",
"panel_back_to_site":"Back to Site", "panel_back_to_site":"Back to Site",
"panel_welcome":"Welcome ", "panel_welcome":"Welcome ",
"panel_menu_head":"Control Panel", "panel_menu_head":"Control Panel",
@ -769,22 +771,24 @@
"panel_user_group":"Group", "panel_user_group":"Group",
"panel_user_update_button":"Update User", "panel_user_update_button":"Update User",
"panel_forums_head":"Forums", "panel.forums_head":"Forums",
"panel_forums_hidden":"Hidden", "panel.forums_hidden":"Hidden",
"panel_forums_edit_button_tooltip":"Edit Forum", "panel.forums_edit_button_tooltip":"Edit Forum",
"panel_forums_edit_button_aria":"Edit Forum", "panel.forums_edit_button_aria":"Edit Forum",
"panel_forums_update_button":"Update", "panel.forums_update_button":"Update",
"panel_forums_delete_button_tooltip":"Delete Forum", "panel.forums_delete_button_tooltip":"Delete Forum",
"panel_forums_delete_button_aria":"Delete Forum", "panel.forums_delete_button_aria":"Delete Forum",
"panel_forums_full_edit_button":"Full Edit", "panel.forums_full_edit_button":"Full Edit",
"panel_forums_create_head":"Add Forum", "panel.forums_create_head":"Add Forum",
"panel_forums_create_name_label":"Name", "panel.forums_create_name_label":"Name",
"panel_forums_create_name":"Super Secret Forum", "panel.forums_create_name":"Super Secret Forum",
"panel_forums_create_description_label":"Description", "panel.forums_create_description_label":"Description",
"panel_forums_create_description":"Where all the super secret stuff happens", "panel.forums_create_description":"Where all the super secret stuff happens",
"panel_forums_active_label":"Active", "panel.forums_active_label":"Active",
"panel_forums_preset_label":"Preset", "panel.forums_preset_label":"Preset",
"panel_forums_create_button":"Add Forum", "panel.forums_create_button":"Add Forum",
"panel.forums_update_order_button":"Update Order",
"panel.forums_order_updated":"The forums have been successfully updated",
"panel_forum_head_suffix":" Forum", "panel_forum_head_suffix":" Forum",
"panel_forum_name":"Name", "panel_forum_name":"Name",
@ -942,8 +946,9 @@
"panel_themes_menus_head":"Menus", "panel_themes_menus_head":"Menus",
"panel_themes_menus_main":"Main Menu", "panel_themes_menus_main":"Main Menu",
"panel_themes_menus_items_head":"Menu Items", "panel_themes_menus_items_head":"Menu Items",
"panel_themes_menus_item_edit_button_aria":"Edit menu item", "panel_themes_menus_items_edit_button_aria":"Edit menu item",
"panel_themes_menus_item_delete_button_aria":"Delete menu item", "panel_themes_menus_items_delete_button_aria":"Delete menu item",
"panel_themes_menus_items_update_button":"Update Order",
"panel_themes_menus_edit_head":"Menu Editor", "panel_themes_menus_edit_head":"Menu Editor",
"panel_themes_menus_create_head":"Create Menu Item", "panel_themes_menus_create_head":"Create Menu Item",

View File

@ -30,6 +30,7 @@ func init() {
addPatch(15, patch15) addPatch(15, patch15)
addPatch(16, patch16) addPatch(16, patch16)
addPatch(17, patch17) addPatch(17, patch17)
addPatch(18, patch18)
} }
func patch0(scanner *bufio.Scanner) (err error) { func patch0(scanner *bufio.Scanner) (err error) {
@ -588,3 +589,7 @@ func patch17(scanner *bufio.Scanner) error {
return err return err
}) })
} }
func patch18(scanner *bufio.Scanner) error {
return execStmt(qgen.Builder.AddColumn("forums", tblColumn{"order", "int", 0, false, false, "0"}, nil))
}

View File

@ -14,6 +14,14 @@ var wsBackoff = 0;
// Topic move // Topic move
var forumToMoveTo = 0; var forumToMoveTo = 0;
function pushNotice(msg) {
let aBox = document.getElementsByClassName("alertbox")[0];
let div = document.createElement('div');
div.innerHTML = Template_notice(msg).trim();
aBox.appendChild(div);
runInitHook("after_notice");
}
// TODO: Write a friendlier error handler which uses a .notice or something, we could have a specialised one for alerts // TODO: Write a friendlier error handler which uses a .notice or something, we could have a specialised one for alerts
function ajaxError(xhr,status,errstr) { function ajaxError(xhr,status,errstr) {
console.log("The AJAX request failed"); console.log("The AJAX request failed");

View File

@ -28,9 +28,9 @@ function runHook(name, ...args) {
console.log("Running hook '"+name+"'"); console.log("Running hook '"+name+"'");
let hook = hooks[name]; let hook = hooks[name];
for (const index in hook) { let ret;
hook[index](...args); for (const index in hook) ret = hook[index](...args);
} return ret;
} }
function addHook(name, callback) { function addHook(name, callback) {
@ -40,15 +40,14 @@ function addHook(name, callback) {
// InitHooks are slightly special, as if they are run, then any adds after the initial run will run immediately, this is to deal with the async nature of script loads // InitHooks are slightly special, as if they are run, then any adds after the initial run will run immediately, this is to deal with the async nature of script loads
function runInitHook(name, ...args) { function runInitHook(name, ...args) {
runHook(name,...args); let ret = runHook(name,...args);
ranInitHooks[name] = true; ranInitHooks[name] = true;
return ret;
} }
function addInitHook(name, callback) { function addInitHook(name, callback) {
addHook(name, callback); addHook(name, callback);
if(name in ranInitHooks) { if(name in ranInitHooks) callback();
callback();
}
} }
// Temporary hack for templates // Temporary hack for templates
@ -175,14 +174,14 @@ function RelativeTime(date) {
return date; return date;
} }
function initPhrases(loggedIn) { function initPhrases(loggedIn, panel = false) {
console.log("in initPhrases") console.log("in initPhrases")
console.log("tmlInits:",tmplInits) console.log("tmlInits:",tmplInits)
let e = ""; let e = "";
if(loggedIn) { if(loggedIn && !panel) e = ",topic_list,topic";
e = ",topic" else if(panel) e = ",analytics,panel"; // TODO: Request phrases for just one section of the control panel?
} else e = ",topic_list";
fetchPhrases("status,topic_list,alerts,paginator,analytics"+e) // TODO: Break this up? fetchPhrases("status,alerts,paginator"+e) // TODO: Break this up?
} }
function fetchPhrases(plist) { function fetchPhrases(plist) {
@ -219,15 +218,18 @@ function fetchPhrases(plist) {
(() => { (() => {
runInitHook("pre_iife"); runInitHook("pre_iife");
let loggedIn = document.head.querySelector("[property='x-loggedin']").content == "true"; let loggedIn = document.head.querySelector("[property='x-loggedin']").content == "true";
let panel = window.location.pathname.startsWith("/panel/");
if(!window.location.pathname.startsWith("/panel/")) { let toLoad = 1;
let toLoad = 2; // TODO: Shunt this into loggedIn if there aren't any search and filter widgets?
// TODO: Shunt this into loggedIn if there aren't any search and filter widgets? let q = (f) => {
let q = (f) => { toLoad--;
toLoad--; if(toLoad===0) initPhrases(loggedIn,panel);
if(toLoad===0) initPhrases(loggedIn); if(f) throw("template function not found");
if(f) throw("template function not found"); };
};
if(!panel) {
toLoad += 2;
if(loggedIn) { if(loggedIn) {
toLoad += 2; toLoad += 2;
notifyOnScriptW("template_topic_c_edit_post", () => q(!Template_topic_c_edit_post)); notifyOnScriptW("template_topic_c_edit_post", () => q(!Template_topic_c_edit_post));
@ -235,9 +237,8 @@ function fetchPhrases(plist) {
} }
notifyOnScriptW("template_topics_topic", () => q(!Template_topics_topic)); notifyOnScriptW("template_topics_topic", () => q(!Template_topics_topic));
notifyOnScriptW("template_paginator", () => q(!Template_paginator)); notifyOnScriptW("template_paginator", () => q(!Template_paginator));
} else {
initPhrases(false);
} }
notifyOnScriptW("template_notice", () => q(!Template_notice));
if(loggedIn) { if(loggedIn) {
fetch("/api/me/") fetch("/api/me/")

59
public/panel_forums.js Normal file
View File

@ -0,0 +1,59 @@
(() => {
addInitHook("end_init", () => {
formVars = {
'forum_active': ['Hide','Show'],
'forum_preset': ['all','announce','members','staff','admins','archive','custom']
};
var forums = {};
let items = document.getElementsByClassName("panel_forum_item");
for(let i = 0; item = items[i]; i++) forums[i] = item.getAttribute("data-fid");
console.log("forums:",forums);
Sortable.create(document.getElementById("panel_forums"), {
sort: true,
onEnd: (evt) => {
console.log("pre forums: ", forums)
console.log("evt: ", evt)
let oldFid = forums[evt.newIndex];
forums[evt.oldIndex] = oldFid;
let newFid = evt.item.getAttribute("data-fid");
console.log("newFid: ", newFid);
forums[evt.newIndex] = newFid;
console.log("post forums: ", forums);
}
});
document.getElementById("panel_forums_order_button").addEventListener("click", () => {
let req = new XMLHttpRequest();
if(!req) {
console.log("Failed to create request");
return false;
}
req.onreadystatechange = () => {
try {
if(req.readyState!==XMLHttpRequest.DONE) return;
// TODO: Signal the error with a notice
if(req.status!==200) return;
let resp = JSON.parse(req.responseText);
console.log("resp: ", resp);
// TODO: Should we move other notices into TmplPhrases like this one?
pushNotice(phraseBox["panel"]["panel.forums_order_updated"]);
if(resp.success==1) return;
} catch(ex) {
console.error("exception: ", ex)
}
console.trace();
}
// ? - Is encodeURIComponent the right function for this?
req.open("POST","/panel/forums/order/edit/submit/?session=" + encodeURIComponent(me.User.Session));
req.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
let items = "";
for(let i = 0; item = forums[i];i++) items += item+",";
if(items.length > 0) items = items.slice(0,-1);
req.send("js=1&amp;items={"+items+"}");
});
});
})();

View File

@ -151,6 +151,7 @@ func panelRoutes() *RouteGroup {
Action("panel.ForumsCreateSubmit", "/panel/forums/create/"), Action("panel.ForumsCreateSubmit", "/panel/forums/create/"),
Action("panel.ForumsDelete", "/panel/forums/delete/", "extraData"), Action("panel.ForumsDelete", "/panel/forums/delete/", "extraData"),
Action("panel.ForumsDeleteSubmit", "/panel/forums/delete/submit/", "extraData"), Action("panel.ForumsDeleteSubmit", "/panel/forums/delete/submit/", "extraData"),
Action("panel.ForumsOrderSubmit", "/panel/forums/order/edit/submit/"),
View("panel.ForumsEdit", "/panel/forums/edit/", "extraData"), View("panel.ForumsEdit", "/panel/forums/edit/", "extraData"),
Action("panel.ForumsEditSubmit", "/panel/forums/edit/submit/", "extraData"), Action("panel.ForumsEditSubmit", "/panel/forums/edit/submit/", "extraData"),
Action("panel.ForumsEditPermsSubmit", "/panel/forums/edit/perms/submit/", "extraData"), Action("panel.ForumsEditPermsSubmit", "/panel/forums/edit/perms/submit/", "extraData"),

View File

@ -145,6 +145,8 @@ var phraseWhitelist = []string{
"alerts", "alerts",
"paginator", "paginator",
"analytics", "analytics",
"panel", // We're going to handle this specially below as this is a security boundary
} }
func routeAPIPhrases(w http.ResponseWriter, r *http.Request, user c.User) c.RouteError { func routeAPIPhrases(w http.ResponseWriter, r *http.Request, user c.User) c.RouteError {
@ -199,13 +201,22 @@ func routeAPIPhrases(w http.ResponseWriter, r *http.Request, user c.User) c.Rout
var ok = false var ok = false
for _, item := range phraseWhitelist { for _, item := range phraseWhitelist {
if strings.HasPrefix(positive, item) { if strings.HasPrefix(positive, item) {
ok = true // TODO: Break this down into smaller security boundaries based on control panel sections?
if strings.HasPrefix(positive,"panel") {
if user.IsSuperMod {
ok = true
w.Header().Set("Cache-Control", "private")
}
} else {
ok = true
}
break break
} }
} }
if !ok { if !ok {
return c.PreErrorJS("Outside of phrase prefix whitelist", w, r) return c.PreErrorJS("Outside of phrase prefix whitelist", w, r)
} }
pPhrases, ok := phrases.GetTmplPhrasesByPrefix(positive) pPhrases, ok := phrases.GetTmplPhrasesByPrefix(positive)
if !ok { if !ok {
return c.PreErrorJS("No such prefix", w, r) return c.PreErrorJS("No such prefix", w, r)
@ -219,13 +230,22 @@ func routeAPIPhrases(w http.ResponseWriter, r *http.Request, user c.User) c.Rout
var ok = false var ok = false
for _, item := range phraseWhitelist { for _, item := range phraseWhitelist {
if strings.HasPrefix(positives[0], item) { if strings.HasPrefix(positives[0], item) {
ok = true // TODO: Break this down into smaller security boundaries based on control panel sections?
if strings.HasPrefix(positives[0],"panel") {
if user.IsSuperMod {
ok = true
w.Header().Set("Cache-Control", "private")
}
} else {
ok = true
}
break break
} }
} }
if !ok { if !ok {
return c.PreErrorJS("Outside of phrase prefix whitelist", w, r) return c.PreErrorJS("Outside of phrase prefix whitelist", w, r)
} }
pPhrases, ok := phrases.GetTmplPhrasesByPrefix(positives[0]) pPhrases, ok := phrases.GetTmplPhrasesByPrefix(positives[0])
if !ok { if !ok {
return c.PreErrorJS("No such prefix", w, r) return c.PreErrorJS("No such prefix", w, r)

View File

@ -22,7 +22,8 @@ type AnalyticsTimeRange struct {
Range string Range string
} }
func analyticsTimeRange(rawTimeRange string) (timeRange AnalyticsTimeRange, err error) { func analyticsTimeRange(rawTimeRange string) (*AnalyticsTimeRange, error) {
timeRange := &AnalyticsTimeRange{}
timeRange.Quantity = 6 timeRange.Quantity = 6
timeRange.Unit = "hour" timeRange.Unit = "hour"
timeRange.Slices = 12 timeRange.Slices = 12
@ -78,7 +79,7 @@ func analyticsTimeRange(rawTimeRange string) (timeRange AnalyticsTimeRange, err
return timeRange, nil return timeRange, nil
} }
func analyticsTimeRangeToLabelList(timeRange AnalyticsTimeRange) (revLabelList []int64, labelList []int64, viewMap map[int64]int64) { func analyticsTimeRangeToLabelList(timeRange *AnalyticsTimeRange) (revLabelList []int64, labelList []int64, viewMap map[int64]int64) {
viewMap = make(map[int64]int64) viewMap = make(map[int64]int64)
var currentTime = time.Now().Unix() var currentTime = time.Now().Unix()
for i := 1; i <= timeRange.Slices; i++ { for i := 1; i <= timeRange.Slices; i++ {

View File

@ -19,6 +19,8 @@ func Forums(w http.ResponseWriter, r *http.Request, user c.User) c.RouteError {
if !user.Perms.ManageForums { if !user.Perms.ManageForums {
return c.NoPermissions(w, r, user) return c.NoPermissions(w, r, user)
} }
basePage.Header.AddScript("Sortable-1.4.0/Sortable.min.js")
basePage.Header.AddScriptAsync("panel_forums.js")
// TODO: Paginate this? // TODO: Paginate this?
var forumList []interface{} var forumList []interface{}
@ -130,6 +132,31 @@ func ForumsDeleteSubmit(w http.ResponseWriter, r *http.Request, user c.User, sfi
return nil return nil
} }
func ForumsOrderSubmit(w http.ResponseWriter, r *http.Request, user c.User) c.RouteError {
_, ferr := c.SimplePanelUserCheck(w, r, &user)
if ferr != nil {
return ferr
}
isJs := (r.PostFormValue("js") == "1")
if !user.Perms.ManageForums {
return c.NoPermissionsJSQ(w, r, user, isJs)
}
sitems := strings.TrimSuffix(strings.TrimPrefix(r.PostFormValue("items"), "{"), "}")
//fmt.Printf("sitems: %+v\n", sitems)
var updateMap = make(map[int]int)
for index, sfid := range strings.Split(sitems, ",") {
fid, err := strconv.Atoi(sfid)
if err != nil {
return c.LocalErrorJSQ("Invalid integer in forum list", w, r, user, isJs)
}
updateMap[fid] = index
}
c.Forums.UpdateOrder(updateMap)
return successRedirect("/panel/forums/", w, r, isJs)
}
func ForumsEdit(w http.ResponseWriter, r *http.Request, user c.User, sfid string) c.RouteError { func ForumsEdit(w http.ResponseWriter, r *http.Request, user c.User, sfid string) c.RouteError {
basePage, ferr := buildBasePage(w, r, &user, "edit_forum", "forums") basePage, ferr := buildBasePage(w, r, &user, "edit_forum", "forums")
if ferr != nil { if ferr != nil {
@ -333,7 +360,7 @@ func ForumsEditPermsAdvance(w http.ResponseWriter, r *http.Request, user c.User,
addNameLangToggle("MoveTopic", forumPerms.MoveTopic) addNameLangToggle("MoveTopic", forumPerms.MoveTopic)
if r.FormValue("updated") == "1" { if r.FormValue("updated") == "1" {
basePage.AddNotice("panel_forums_perms_updated") basePage.AddNotice("panel_forum_perms_updated")
} }
pi := c.PanelEditForumGroupPage{basePage, forum.ID, gid, forum.Name, forum.Desc, forum.Active, forum.Preset, formattedPermList} pi := c.PanelEditForumGroupPage{basePage, forum.ID, gid, forum.Name, forum.Desc, forum.Active, forum.Preset, formattedPermList}

View File

@ -3,6 +3,7 @@ CREATE TABLE [forums] (
[name] nvarchar (100) not null, [name] nvarchar (100) not null,
[desc] nvarchar (200) not null, [desc] nvarchar (200) not null,
[active] bit DEFAULT 1 not null, [active] bit DEFAULT 1 not null,
[order] int DEFAULT 0 not null,
[topicCount] int DEFAULT 0 not null, [topicCount] int DEFAULT 0 not null,
[preset] nvarchar (100) DEFAULT '' not null, [preset] nvarchar (100) DEFAULT '' not null,
[parentID] int DEFAULT 0 not null, [parentID] int DEFAULT 0 not null,

View File

@ -3,6 +3,7 @@ CREATE TABLE `forums` (
`name` varchar(100) not null, `name` varchar(100) not null,
`desc` varchar(200) not null, `desc` varchar(200) not null,
`active` boolean DEFAULT 1 not null, `active` boolean DEFAULT 1 not null,
`order` int DEFAULT 0 not null,
`topicCount` int DEFAULT 0 not null, `topicCount` int DEFAULT 0 not null,
`preset` varchar(100) DEFAULT '' not null, `preset` varchar(100) DEFAULT '' not null,
`parentID` int DEFAULT 0 not null, `parentID` int DEFAULT 0 not null,

View File

@ -3,6 +3,7 @@ CREATE TABLE "forums" (
`name` varchar (100) not null, `name` varchar (100) not null,
`desc` varchar (200) not null, `desc` varchar (200) not null,
`active` boolean DEFAULT 1 not null, `active` boolean DEFAULT 1 not null,
`order` int DEFAULT 0 not null,
`topicCount` int DEFAULT 0 not null, `topicCount` int DEFAULT 0 not null,
`preset` varchar (100) DEFAULT '' not null, `preset` varchar (100) DEFAULT '' not null,
`parentID` int DEFAULT 0 not null, `parentID` int DEFAULT 0 not null,

View File

@ -7,7 +7,7 @@
{{range .Header.PreScriptsAsync}} {{range .Header.PreScriptsAsync}}
<script async type="text/javascript" src="/static/{{.}}"></script>{{end}} <script async type="text/javascript" src="/static/{{.}}"></script>{{end}}
<meta property="x-loggedin" content="{{.CurrentUser.Loggedin}}" /> <meta property="x-loggedin" content="{{.CurrentUser.Loggedin}}" />
<script type="text/javascript" src="/static/init.js?i=3"></script> <script type="text/javascript" src="/static/init.js?i=4"></script>
{{range .Header.ScriptsAsync}} {{range .Header.ScriptsAsync}}
<script async type="text/javascript" src="/static/{{.}}"></script>{{end}} <script async type="text/javascript" src="/static/{{.}}"></script>{{end}}
<script type="text/javascript" src="/static/jquery-3.1.1.min.js"></script> <script type="text/javascript" src="/static/jquery-3.1.1.min.js"></script>
@ -25,7 +25,7 @@
{{if .GoogSiteVerify}}<meta name="google-site-verification" content="{{.GoogSiteVerify}}" />{{end}} {{if .GoogSiteVerify}}<meta name="google-site-verification" content="{{.GoogSiteVerify}}" />{{end}}
</head> </head>
<body> <body>
{{if not .CurrentUser.IsSuperMod}}<style>.supermod_only { display: none !important; }</style>{{end}} {{if not .CurrentUser.IsSuperMod}}<style>.supermod_only { display: none !important; }</style>{{end}}{{flush}}
<div id="container" class="container"> <div id="container" class="container">
{{/**<!--<div class="navrow">-->**/}} {{/**<!--<div class="navrow">-->**/}}
<div class="left_of_nav">{{dock "leftOfNav" .Header }}</div> <div class="left_of_nav">{{dock "leftOfNav" .Header }}</div>

View File

@ -2,60 +2,63 @@
<div class="colstack panel_stack"> <div class="colstack panel_stack">
{{template "panel_menu.html" . }} {{template "panel_menu.html" . }}
<script>var formVars = {
'forum_active': ['Hide','Show'],
'forum_preset': ['all','announce','members','staff','admins','archive','custom']};
</script>
<main class="colstack_right"> <main class="colstack_right">
{{template "panel_before_head.html" . }} {{template "panel_before_head.html" . }}
<div class="colstack_item colstack_head"> <div class="colstack_item colstack_head">
<div class="rowitem"><h1>{{lang "panel_forums_head"}}</h1></div> <div class="rowitem">
<h1>{{lang "panel.forums_head"}}</h1>
<h2 class="hguide">{{lang "panel_hints_reorder"}}</h2>
</div>
</div> </div>
<div id="panel_forums" class="colstack_item rowlist"> <div id="panel_forums" class="colstack_item rowlist">
{{range .ItemList}} {{range .ItemList}}
<div class="rowitem editable_parent"> <div data-fid="{{.ID}}" class="rowitem editable_parent panel_forum_item{{if not .Desc}} forum_no_desc{{end}}">
<span class="grip"></span>
<span id="panel_forums_left_box"> <span id="panel_forums_left_box">
{{/** TODO: Make sure the forum_active_name class is set and unset when the activity status of this forum is changed **/}} {{/** TODO: Make sure the forum_active_name class is set and unset when the activity status of this forum is changed **/}}
<a data-field="forum_name" data-type="text" class="editable_block forum_name{{if not .Active}} forum_active_name{{end}}">{{.Name}}</a> <a data-field="forum_name" data-type="text" class="editable_block forum_name{{if not .Active}} forum_active_name{{end}}">{{.Name}}</a>
<br /><span data-field="forum_desc" data-type="text" class="editable_block forum_desc rowsmall">{{.Desc}}</span> <br /><span data-field="forum_desc" data-type="text" class="editable_block forum_desc rowsmall">{{.Desc}}</span>
</span> </span>
<span class="panel_floater"> <span class="panel_floater">
<span data-field="forum_active" data-type="list" class="panel_tag editable_block forum_active {{if .Active}}forum_active_Show" data-value="1{{else}}forum_active_Hide" data-value="0{{end}}" title="{{lang "panel_forums_hidden"}}"></span> <span data-field="forum_active" data-type="list" class="panel_tag editable_block forum_active forum_active_{{if .Active}}Show" data-value="1{{else}}Hide" data-value="0{{end}}" title="{{lang "panel.forums_hidden"}}"></span>
<span data-field="forum_preset" data-type="list" data-value="{{.Preset}}" class="panel_tag editable_block forum_preset forum_preset_{{.Preset}}" title="{{.PresetLang}}"></span> <span data-field="forum_preset" data-type="list" data-value="{{.Preset}}" class="panel_tag editable_block forum_preset forum_preset_{{.Preset}}" title="{{.PresetLang}}"></span>
</span> </span>
<span class="panel_buttons"> <span class="panel_buttons">
<a class="panel_tag edit_fields hide_on_edit panel_right_button edit_button" title="{{lang "panel_forums_edit_button_tooltip"}}" aria-label="{{lang "panel_forums_edit_button_aria"}}"></a> <a class="panel_tag edit_fields hide_on_edit panel_right_button edit_button" title="{{lang "panel.forums_edit_button_tooltip"}}" aria-label="{{lang "panel.forums_edit_button_aria"}}"></a>
<a class="panel_right_button has_inner_button show_on_edit" href="/panel/forums/edit/submit/{{.ID}}"><button class='panel_tag submit_edit' type='submit'>{{lang "panel_forums_update_button"}}</button></a> <a class="panel_right_button has_inner_button show_on_edit" href="/panel/forums/edit/submit/{{.ID}}"><button class='panel_tag submit_edit' type='submit'>{{lang "panel.forums_update_button"}}</button></a>
{{if gt .ID 1}}<a href="/panel/forums/delete/{{.ID}}?session={{$.CurrentUser.Session}}" class="panel_tag panel_right_button hide_on_edit delete_button" title="{{lang "panel_forums_delete_button_tooltip"}}" aria-label="{{lang "panel_forums_delete_button_aria"}}"></a>{{end}} {{if gt .ID 1}}<a href="/panel/forums/delete/{{.ID}}?session={{$.CurrentUser.Session}}" class="panel_tag panel_right_button hide_on_edit delete_button" title="{{lang "panel.forums_delete_button_tooltip"}}" aria-label="{{lang "panel.forums_delete_button_aria"}}"></a>{{end}}
<a href="/panel/forums/edit/{{.ID}}" class="panel_tag panel_right_button has_inner_button show_on_edit"><button>{{lang "panel_forums_full_edit_button"}}</button></a> <a href="/panel/forums/edit/{{.ID}}" class="panel_tag panel_right_button has_inner_button show_on_edit"><button>{{lang "panel.forums_full_edit_button"}}</button></a>
</span> </span>
</div> </div>
{{end}} {{end}}
</div> </div>
<div class="colstack_item rowlist panel_submitrow">
<div class="rowitem"><button id="panel_forums_order_button" class="formbutton">{{lang "panel.forums_update_order_button"}}</button></div>
</div>
<div class="colstack_item colstack_head"> <div class="colstack_item colstack_head">
<div class="rowitem"><h1>{{lang "panel_forums_create_head"}}</h1></div> <div class="rowitem"><h1>{{lang "panel.forums_create_head"}}</h1></div>
</div> </div>
<div class="colstack_item the_form"> <div class="colstack_item the_form">
<form action="/panel/forums/create/?session={{.CurrentUser.Session}}" method="post"> <form action="/panel/forums/create/?session={{.CurrentUser.Session}}" method="post">
<div class="formrow"> <div class="formrow">
<div class="formitem formlabel"><a>{{lang "panel_forums_create_name_label"}}</a></div> <div class="formitem formlabel"><a>{{lang "panel.forums_create_name_label"}}</a></div>
<div class="formitem"><input name="forum-name" type="text" placeholder="{{lang "panel_forums_create_name"}}" /></div> <div class="formitem"><input name="forum-name" type="text" placeholder="{{lang "panel.forums_create_name"}}" /></div>
</div> </div>
<div class="formrow"> <div class="formrow">
<div class="formitem formlabel"><a>{{lang "panel_forums_create_description_label"}}</a></div> <div class="formitem formlabel"><a>{{lang "panel.forums_create_description_label"}}</a></div>
<div class="formitem"><input name="forum-desc" type="text" placeholder="{{lang "panel_forums_create_description"}}" /></div> <div class="formitem"><input name="forum-desc" type="text" placeholder="{{lang "panel.forums_create_description"}}" /></div>
</div> </div>
<div class="formrow"> <div class="formrow">
<div class="formitem formlabel"><a>{{lang "panel_forums_active_label"}}</a></div> <div class="formitem formlabel"><a>{{lang "panel.forums_active_label"}}</a></div>
<div class="formitem"><select name="forum-active"> <div class="formitem"><select name="forum-active">
<option selected value="1">{{lang "option_yes"}}</option> <option selected value="1">{{lang "option_yes"}}</option>
<option value="0">{{lang "option_no"}}</option> <option value="0">{{lang "option_no"}}</option>
</select></div> </select></div>
</div> </div>
<div class="formrow"> <div class="formrow">
<div class="formitem formlabel"><a>{{lang "panel_forums_preset_label"}}</a></div> <div class="formitem formlabel"><a>{{lang "panel.forums_preset_label"}}</a></div>
<div class="formitem"><select name="forum-preset"> <div class="formitem"><select name="forum-preset">
<option selected value="all">{{lang "panel_preset_everyone"}}</option> <option selected value="all">{{lang "panel_preset_everyone"}}</option>
<option value="announce">{{lang "panel_preset_announcements"}}</option> <option value="announce">{{lang "panel_preset_announcements"}}</option>
@ -67,7 +70,7 @@
</select></div> </select></div>
</div> </div>
<div class="formrow"> <div class="formrow">
<div class="formitem"><button name="panel-button" class="formbutton">{{lang "panel_forums_create_button"}}</button></div> <div class="formitem"><button name="panel-button" class="formbutton">{{lang "panel.forums_create_button"}}</button></div>
</div> </div>
</form> </form>
</div> </div>

View File

@ -4,20 +4,24 @@
<main class="colstack_right"> <main class="colstack_right">
{{template "panel_before_head.html" . }} {{template "panel_before_head.html" . }}
<div class="colstack_item colstack_head"> <div class="colstack_item colstack_head">
<div class="rowitem"><h1>{{lang "panel_themes_menus_items_head"}}</h1></div> <div class="rowitem">
<h1>{{lang "panel_themes_menus_items_head"}}</h1>
<h2 class="hguide">{{lang "panel_hints_reorder"}}</h2>
</div>
</div> </div>
<div id="panel_menu_item_holder" class="colstack_item rowlist"> <div id="panel_menu_item_holder" class="colstack_item rowlist">
{{range .ItemList}} {{range .ItemList}}
<div class="panel_menu_item rowitem panel_compactrow editable_parent" data-miid="{{.ID}}"> <div class="panel_menu_item rowitem panel_compactrow editable_parent" data-miid="{{.ID}}">
<span class="grip"></span>
<a href="/panel/themes/menus/item/edit/{{.ID}}" class="editable_block panel_upshift">{{.Name}}</a> <a href="/panel/themes/menus/item/edit/{{.ID}}" class="editable_block panel_upshift">{{.Name}}</a>
<span class="panel_buttons"> <span class="panel_buttons">
<a href="/panel/themes/menus/item/edit/{{.ID}}" class="panel_tag panel_right_button edit_button" aria-label="{{lang "panel_themes_menus_item_edit_button_aria"}}"></a> <a href="/panel/themes/menus/item/edit/{{.ID}}" class="panel_tag panel_right_button edit_button" aria-label="{{lang "panel_themes_menus_items_edit_button_aria"}}"></a>
<a href="/panel/themes/menus/item/delete/submit/{{.ID}}?session={{$.CurrentUser.Session}}" class="panel_tag panel_right_button delete_button" aria-label="{{lang "panel_themes_menus_item_delete_button_aria"}}"></a> <a href="/panel/themes/menus/item/delete/submit/{{.ID}}?session={{$.CurrentUser.Session}}" class="panel_tag panel_right_button delete_button" aria-label="{{lang "panel_themes_menus_items_delete_button_aria"}}"></a>
</span> </span>
</div>{{end}} </div>{{end}}
</div> </div>
<div class="colstack_item rowlist panel_submitrow"> <div class="colstack_item rowlist panel_submitrow">
<div class="rowitem"><button id="panel_menu_items_order_button" class="formbutton">{{lang "panel_themes_menus_edit_update_button"}}</button></div> <div class="rowitem"><button id="panel_menu_items_order_button" class="formbutton">{{lang "panel_themes_menus_items_update_button"}}</button></div>
</div> </div>
<div class="colstack_item colstack_head"> <div class="colstack_item colstack_head">
<div class="rowitem"><h1>{{lang "panel_themes_menus_create_head"}}</h1></div> <div class="rowitem"><h1>{{lang "panel_themes_menus_create_head"}}</h1></div>
@ -80,10 +84,8 @@
// TODO: Move this into a JS file to reduce the number of possible problems // TODO: Move this into a JS file to reduce the number of possible problems
var menuItems = {}; var menuItems = {};
let items = document.getElementsByClassName("panel_menu_item"); let items = document.getElementsByClassName("panel_menu_item");
for(let i = 0; item = items[i];i++) { for(let i = 0; item = items[i]; i++) menuItems[i] = item.getAttribute("data-miid");
let miid = item.getAttribute("data-miid");
menuItems[i] = miid;
}
Sortable.create(document.getElementById("panel_menu_item_holder"), { Sortable.create(document.getElementById("panel_menu_item_holder"), {
sort: true, sort: true,
onEnd: (evt) => { onEnd: (evt) => {
@ -92,11 +94,12 @@ Sortable.create(document.getElementById("panel_menu_item_holder"), {
let oldMiid = menuItems[evt.newIndex]; let oldMiid = menuItems[evt.newIndex];
menuItems[evt.oldIndex] = oldMiid; menuItems[evt.oldIndex] = oldMiid;
let newMiid = evt.item.getAttribute("data-miid"); let newMiid = evt.item.getAttribute("data-miid");
console.log("newMiid: ", newMiid) console.log("newMiid: ", newMiid);
menuItems[evt.newIndex] = newMiid; menuItems[evt.newIndex] = newMiid;
console.log("post menuItems: ", menuItems) console.log("post menuItems: ", menuItems);
} }
}); });
document.getElementById("panel_menu_items_order_button").addEventListener("click", () => { document.getElementById("panel_menu_items_order_button").addEventListener("click", () => {
let req = new XMLHttpRequest(); let req = new XMLHttpRequest();
if(!req) { if(!req) {
@ -105,18 +108,13 @@ document.getElementById("panel_menu_items_order_button").addEventListener("click
} }
req.onreadystatechange = () => { req.onreadystatechange = () => {
try { try {
if(req.readyState!==XMLHttpRequest.DONE) { if(req.readyState!==XMLHttpRequest.DONE) return;
return;
}
// TODO: Signal the error with a notice // TODO: Signal the error with a notice
if(req.status===200) { if(req.status===200) {
let resp = JSON.parse(req.responseText); let resp = JSON.parse(req.responseText);
console.log("resp: ", resp); console.log("resp: ", resp);
if(resp.success==1) { // TODO: Have a successfully updated notice
// TODO: Have a successfully updated notice if(resp.success==1) return;
console.log("success");
return;
}
} }
} catch(ex) { } catch(ex) {
console.error("exception: ", ex) console.error("exception: ", ex)
@ -127,12 +125,8 @@ document.getElementById("panel_menu_items_order_button").addEventListener("click
req.open("POST","/panel/themes/menus/item/order/edit/submit/{{.MenuID}}?session=" + encodeURIComponent(me.User.Session)); req.open("POST","/panel/themes/menus/item/order/edit/submit/{{.MenuID}}?session=" + encodeURIComponent(me.User.Session));
req.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); req.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
let items = ""; let items = "";
for(let i = 0; item = menuItems[i];i++) { for(let i = 0; item = menuItems[i];i++) items += item+",";
items += item+","; if(items.length > 0) items = items.slice(0,-1);
}
if(items.length > 0) {
items = items.slice(0,-1);
}
req.send("js=1&amp;items={"+items+"}"); req.send("js=1&amp;items={"+items+"}");
}); });
</script> </script>

View File

@ -62,6 +62,12 @@
/*margin-top: -4px;*/ /*margin-top: -4px;*/
margin-bottom: 14px; margin-bottom: 14px;
} }
.colstack_right .colstack_head .rowitem {
display: flex;
}
.colstack_right .colstack_head h1 + h2.hguide {
margin-left: auto;
}
.footer { .footer {
margin-top: 0px; margin-top: 0px;
} }

View File

@ -358,6 +358,11 @@ h2 {
margin-bottom: 8px; margin-bottom: 8px;
margin-left: 8px; margin-left: 8px;
} }
.rowhead h2, .colstack_head h2 {
margin-top: 0px;
margin-bottom: 0px;
margin-left: 0px;
}
.topic_create_form { .topic_create_form {
display: flex; display: flex;

View File

@ -30,6 +30,17 @@ function noxMenuBind() {
} }
(() => { (() => {
function moveAlerts() {
// Move the alerts above the first header
let colSel = $(".colstack_right .colstack_head:first");
let colSelAlt = $(".colstack_right .colstack_item:first");
let colSelAltAlt = $(".colstack_right .coldyn_block:first");
if(colSel.length > 0) $('.alert').insertBefore(colSel);
else if (colSelAlt.length > 0) $('.alert').insertBefore(colSelAlt);
else if (colSelAltAlt.length > 0) $('.alert').insertBefore(colSelAltAlt);
else $('.alert').insertAfter(".rowhead:first");
}
addInitHook("after_update_alert_list", (alertCount) => { addInitHook("after_update_alert_list", (alertCount) => {
console.log("misc.js"); console.log("misc.js");
console.log("alertCount:",alertCount); console.log("alertCount:",alertCount);
@ -57,15 +68,7 @@ function noxMenuBind() {
$(window).resize(() => noxMenuBind()); $(window).resize(() => noxMenuBind());
noxMenuBind(); noxMenuBind();
moveAlerts();
// Move the alerts above the first header
let colSel = $(".colstack_right .colstack_head:first");
let colSelAlt = $(".colstack_right .colstack_item:first");
let colSelAltAlt = $(".colstack_right .coldyn_block:first");
if(colSel.length > 0) $('.alert').insertBefore(colSel);
else if (colSelAlt.length > 0) $('.alert').insertBefore(colSelAlt);
else if (colSelAltAlt.length > 0) $('.alert').insertBefore(colSelAltAlt);
else $('.alert').insertAfter(".rowhead:first");
$(".menu_hamburger").click(function() { $(".menu_hamburger").click(function() {
event.stopPropagation(); event.stopPropagation();
@ -78,4 +81,6 @@ function noxMenuBind() {
$(document).click(() => $(".more_menu").removeClass("more_menu_selected")); $(document).click(() => $(".more_menu").removeClass("more_menu_selected"));
}); });
addInitHook("after_notice", moveAlerts);
})(); })();

View File

@ -79,6 +79,10 @@
.colstack_right .colstack_head h1 { .colstack_right .colstack_head h1 {
font-size: 21px; font-size: 21px;
} }
.colstack_right .colstack_head h1 + h2.hguide {
margin-left: auto;
font-size: 17px;
}
.colstack_right .colstack_item.the_form, .colstack_right .colstack_item:not(.colstack_head):not(.rowhead) .rowitem { .colstack_right .colstack_item.the_form, .colstack_right .colstack_item:not(.colstack_head):not(.rowhead) .rowitem {
background-color: #444444; background-color: #444444;
} }
@ -292,6 +296,35 @@ button, .formbutton, .panel_right_button:not(.has_inner_button), #panel_users .p
margin-left: 4px; margin-left: 4px;
} }
span.grip {
content: '....';
width: 20px;
display: inline-block;
overflow: hidden;
line-height: 5px;
padding: 3px 4px;
cursor: move;
vertical-align: middle;
margin-top: -16px;
margin-right: 12px;
font-size: 12px;
font-family: sans-serif;
letter-spacing: -3px;
color: #888888;
text-shadow: 1px 0 1px black;
margin-left: -12px;
height: 100%;
font-size: 40px;
margin-bottom: -4px;
line-height: 8px;
}
span.grip::after {
content: '... ... ... ... ... ... ...';
}
.forum_no_desc span.grip, .panel_menu_item span.grip {
height: 40px;
}
.panel_plugin_meta { .panel_plugin_meta {
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View File

@ -11,6 +11,9 @@
.colstack_head .rowitem a h1 { .colstack_head .rowitem a h1 {
margin-right: 0px; margin-right: 0px;
} }
.rowitem h2.hguide {
font-size: 15px;
}
.rowlist .tag-mini { .rowlist .tag-mini {
font-size: 10px; font-size: 10px;

View File

@ -10,6 +10,12 @@
.submenu a { .submenu a {
margin-left: 8px; margin-left: 8px;
} }
/*.colstack_right .colstack_head .rowitem {
display: flex;
}*/
.colstack_right .colstack_head h1 + h2.hguide {
margin-left: auto;
}
.edit_button:before { .edit_button:before {
content: "{{lang "panel_edit_button_text" . }}"; content: "{{lang "panel_edit_button_text" . }}";