~evanj/cms

9d4babd44d42cef541545d64cd13dc6628c6331d — Evan M Jones 7 months ago 5387b45
Feat(contenttype): Content Types can now be updated.
M internal/c/contenttype/contenttype.go => internal/c/contenttype/contenttype.go +118 -13
@@ 32,22 32,32 @@ type dber interface {
	SpaceGet(user user.User, spaceID string) (space.Space, error)
	ContentTypeNew(space space.Space, name string, params []db.ContentTypeNewParam) (contenttype.ContentType, error)
	ContentTypeGet(space space.Space, contenttypeID string) (contenttype.ContentType, error)
	ContentTypeUpdate(space space.Space, contenttype contenttype.ContentType, name string, newParams []db.ContentTypeNewParam, updateParams []db.ContentTypeUpdateParam) (contenttype.ContentType, error)
	ContentTypeDelete(space space.Space, ct contenttype.ContentType) error
	ContentTypeSearch(space space.Space, query string, page int) ([]contenttype.ContentType, error)
	ContentPerContentType(space space.Space, ct contenttype.ContentType, page int, order db.OrderType, sortField string) ([]content.Content, error)
}

func New(log *log.Logger, db dber) *ContentType {
	return &ContentType{
		c.New(log, db),
		log,
		db,
	}
	return &ContentType{c.New(log, db), log, db}
}

func (c *ContentType) tree(w http.ResponseWriter, r *http.Request) (user.User, space.Space, error) {
	user, err := c.GetCookieUser(w, r)
	if err != nil {
		return nil, nil, fmt.Errorf("must be logged in to perform this action")
	}

	spaceID := r.FormValue("space")
	space, err := c.db.SpaceGet(user, spaceID)
	if err != nil {
		return nil, nil, fmt.Errorf("failed to retrieve space for id %d", spaceID)
	}

	return user, space, nil
}
func (c *ContentType) create(w http.ResponseWriter, r *http.Request) {
	name := r.FormValue("name")
	spaceID := r.FormValue("space")

	i := 1
	for key := range r.Form {


@@ 77,21 87,101 @@ func (c *ContentType) create(w http.ResponseWriter, r *http.Request) {
		return
	}

	user, err := c.GetCookieUser(w, r)
	// Enforce content always has a value for value type of "name"
	hasName := false
	for _, p := range params {
		if p.Name == "name" {
			hasName = true
		}
	}
	if !hasName {
		c.Error(w, r, http.StatusInternalServerError, "must have field of \"name\" for searchability")
		return
	}

	user, space, err := c.tree(w, r)
	if err != nil {
		c.Error(w, r, http.StatusBadRequest, "must be logged in to create contenttype")
		c.Error(w, r, http.StatusBadRequest, err.Error())
		return
	}

	space, err := c.db.SpaceGet(user, spaceID)
	ct, err := c.db.ContentTypeNew(space, name, params)
	if err != nil {
		c.Error(w, r, http.StatusInternalServerError, "failed to find desired space")
		c.log.Println(err)
		c.Error(w, r, http.StatusInternalServerError, "failed to create contenttype")
		return
	}

	url := fmt.Sprintf("/contenttype/%s/%s", space.ID(), ct.ID())
	c.log.Println("successfully created new contenttype for user", user.Name(), "in space", space.Name(), "redirecting to", url)
	http.Redirect(w, r, url, http.StatusTemporaryRedirect)
}

func (c *ContentType) update(w http.ResponseWriter, r *http.Request) {
	name := r.FormValue("name")
	ctID := r.FormValue("contenttype")

	// Gather new params.

	i := 1
	for key := range r.Form {
		if strings.Contains(key, "field_type_") || strings.Contains(key, "field_name_") {
			i++
		}
	}

	var newParams []db.ContentTypeNewParam
	for e := 0; e < (i / 2); e++ {
		keyName := fmt.Sprintf("field_name_%d", e+2)
		keyType := fmt.Sprintf("field_type_%d", e+2)
		valName := r.FormValue(keyName)
		valType := r.FormValue(keyType)
		if valName == "" || valType == "" {
			c.Error(w, r, http.StatusBadRequest, "form has malformed data")
			return
		}
		newParams = append(newParams, db.ContentTypeNewParam{
			Name: valName,
			Type: valType,
		})
	}

	// Gather updated params.

	i = 1
	for key := range r.Form {
		if strings.Contains(key, "field_update_type_") || strings.Contains(key, "field_update_name_") || strings.Contains(key, "field_update_id_") {
			i++
		}
	}

	var updateParams []db.ContentTypeUpdateParam
	for e := 0; e < (i / 3); e++ {
		keyID := fmt.Sprintf("field_update_id_%d", e+1)
		keyName := fmt.Sprintf("field_update_name_%d", e+1)
		keyType := fmt.Sprintf("field_update_type_%d", e+1)
		valID := r.FormValue(keyID)
		valName := r.FormValue(keyName)
		valType := r.FormValue(keyType)
		if valName == "" || valType == "" || valID == "" {
			c.Error(w, r, http.StatusBadRequest, "form has malformed data")
			return
		}
		updateParams = append(updateParams, db.ContentTypeUpdateParam{
			ID:   valID,
			Name: valName,
			Type: valType,
		})
	}

	if len(updateParams) < 1 {
		c.Error(w, r, http.StatusBadRequest, "contenttype must have at least one field")
		return
	}

	// Enforce content always has a value for value type of "name"
	hasName := false
	for _, p := range params {
	for _, p := range updateParams {
		if p.Name == "name" {
			hasName = true
		}


@@ 101,7 191,19 @@ func (c *ContentType) create(w http.ResponseWriter, r *http.Request) {
		return
	}

	ct, err := c.db.ContentTypeNew(space, name, params)
	user, space, err := c.tree(w, r)
	if err != nil {
		c.Error(w, r, http.StatusBadRequest, err.Error())
		return
	}

	ct, err := c.db.ContentTypeGet(space, ctID)
	if err != nil {
		c.Error(w, r, http.StatusInternalServerError, "failed to find required contenttype")
		return
	}

	ct, err = c.db.ContentTypeUpdate(space, ct, name, newParams, updateParams)
	if err != nil {
		c.log.Println(err)
		c.Error(w, r, http.StatusInternalServerError, "failed to create contenttype")


@@ 109,7 211,7 @@ func (c *ContentType) create(w http.ResponseWriter, r *http.Request) {
	}

	url := fmt.Sprintf("/contenttype/%s/%s", space.ID(), ct.ID())
	c.log.Println("successfully created new contenttype for user", user.Name(), "in space", space.Name(), "redirecting to", url)
	c.log.Println("successfully updated contenttype for user", user.Name(), "in space", space.Name(), "redirecting to", url)
	http.Redirect(w, r, url, http.StatusTemporaryRedirect)
}



@@ 242,6 344,9 @@ func (c *ContentType) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	case "/contenttype/new":
		c.create(w, r)
		return
	case "/contenttype/update":
		c.update(w, r)
		return
	case "/contenttype/delete":
		c.delete(w, r)
		return

M internal/s/cache/contenttype.go => internal/s/cache/contenttype.go +13 -0
@@ 34,6 34,19 @@ func (c *Cache) ContentTypeNew(space space.Space, name string, params []db.Conte
	)
}

func (c *Cache) ContentTypeUpdate(space space.Space, thing contenttype.ContentType, name string, newParams []db.ContentTypeNewParam, updateParams []db.ContentTypeUpdateParam) (contenttype.ContentType, error) {
	thing, err := c.db.ContentTypeUpdate(space, thing, name, newParams, updateParams)
	if err != nil {
		return nil, err
	}

	return c.contenttype(
		true,
		fmt.Sprintf("%s::%s::%s", c.baseKey, space.ID(), thing.ID()),
		func() (contenttype.ContentType, error) { return thing, err },
	)
}

func (c *Cache) ContentTypeGet(space space.Space, thingID string) (contenttype.ContentType, error) {
	return c.contenttype(
		false,

M internal/s/db/content.go => internal/s/db/content.go +20 -0
@@ 61,6 61,26 @@ var (

		// Need delete from w/ joins per final cms_value_* table.

		`DELETE FROM cms_value_reference WHERE ID IN ( 
			SELECT VALUE_ID FROM cms_value 
			JOIN cms_contenttype_to_valuetype
			ON cms_contenttype_to_valuetype.ID = CONTENTTYPE_TO_VALUETYPE_ID
			JOIN cms_valuetype
			ON cms_valuetype.ID = VALUETYPE_ID 
			WHERE CONTENT_ID = ?
			AND cms_valuetype.VALUE = 'Reference'
		)`,

		`DELETE FROM cms_value_reference_list WHERE ID IN ( 
			SELECT VALUE_ID FROM cms_value 
			JOIN cms_contenttype_to_valuetype
			ON cms_contenttype_to_valuetype.ID = CONTENTTYPE_TO_VALUETYPE_ID
			JOIN cms_valuetype
			ON cms_valuetype.ID = VALUETYPE_ID 
			WHERE CONTENT_ID = ?
			AND cms_valuetype.VALUE = 'ReferenceList'
		)`,

		`DELETE FROM cms_value_string_small WHERE ID IN ( 
			SELECT VALUE_ID FROM cms_value 
			JOIN cms_contenttype_to_valuetype

M internal/s/db/contenttype.go => internal/s/db/contenttype.go +86 -0
@@ 2,6 2,8 @@ package db

import (
	"fmt"
	"strconv"
	"strings"

	"git.sr.ht/~evanj/cms/internal/m/contenttype"
	"git.sr.ht/~evanj/cms/internal/m/space"


@@ 24,6 26,10 @@ type ContentTypeNewParam struct {
	Name, Type string
}

type ContentTypeUpdateParam struct {
	ID, Name, Type string
}

var (
	queryCreateContentType             = `INSERT INTO cms_contenttype (NAME, SPACE_ID) VALUES (?, ?);`
	queryDeleteContentType             = `DELETE FROM cms_contenttype WHERE ID = ?;`


@@ 33,6 39,33 @@ var (
	queryFindContentTypesBySpace       = `SELECT ID, NAME FROM cms_contenttype WHERE SPACE_ID = ? LIMIT ? OFFSET ?;`
	queryCreateContentTypeConnection   = `INSERT INTO cms_contenttype_to_valuetype (NAME, CONTENTTYPE_ID, VALUETYPE_ID) VALUES (?, ?, ( SELECT ID FROM cms_valuetype WHERE VALUE = ? ));`
	queryFindValueTypes                = `SELECT cms_contenttype_to_valuetype.ID, NAME, VALUE FROM cms_contenttype_to_valuetype JOIN cms_valuetype ON VALUETYPE_ID = cms_valuetype.ID WHERE CONTENTTYPE_ID = ? ORDER BY cms_contenttype_to_valuetype.ID ASC;`

	queryDeleteContentTypeFieldsNotIn = func(values []int) string {
		// WARNING, DANGEROUS. SQL injection will not occur here because we converted
		// to ints, but be careful of updating this code in the future.
		var fieldStringIDs []string
		for _, i := range values {
			str := strconv.Itoa(i)
			fieldStringIDs = append(fieldStringIDs, str)
		}

		return fmt.Sprintf(`DELETE FROM cms_contenttype_to_valuetype 
			WHERE CONTENTTYPE_ID = ?
			AND ID NOT IN (%s)
		`, strings.Join(fieldStringIDs, ", "))
	}

	queryUpdateContentTypeFieldNames = `
		UPDATE cms_contenttype_to_valuetype
		SET NAME = ?
		WHERE ID = ?
	`

	queryUpdateContentTypeName = `
		UPDATE cms_contenttype
		SET NAME = ?
		WHERE ID = ?
	`
)

func (db *DB) ContentTypeNew(space space.Space, name string, params []ContentTypeNewParam) (contenttype.ContentType, error) {


@@ 82,6 115,59 @@ func (db *DB) ContentTypeNew(space space.Space, name string, params []ContentTyp
	return &ct, t.Commit()
}

// ContentTypeUpdate will either remove fields are add fields to a contenttype.
// Note: field types cannot be changed (e.g. if a field is of type InputHTML it
// cannot become a StringSmall. Field type names can be changed.
func (db *DB) ContentTypeUpdate(space space.Space, contenttype contenttype.ContentType, name string, newParams []ContentTypeNewParam, updateParams []ContentTypeUpdateParam) (contenttype.ContentType, error) {
	t, err := db.Begin()
	if err != nil {
		return nil, err
	}
	defer t.Rollback()

	// Gather update param IDs, delete those not within.
	// Update the names of already existing fields.

	var fieldIDs []int
	for _, p := range updateParams {
		_, err := t.Exec(queryUpdateContentTypeFieldNames, p.Name, p.ID)
		if err != nil {
			return nil, err
		}

		id, err := strconv.Atoi(p.ID)
		if err != nil {
			return nil, err
		}

		fieldIDs = append(fieldIDs, id)
	}

	if _, err := t.Exec(queryDeleteContentTypeFieldsNotIn(fieldIDs), contenttype.ID()); err != nil {
		return nil, err
	}

	// Update name of CT itself.

	if _, err := t.Exec(queryUpdateContentTypeName, name, contenttype.ID()); err != nil {
		return nil, err
	}

	// Create new fields given.

	for _, item := range newParams {
		if _, err := t.Exec(queryCreateContentTypeConnection, item.Name, contenttype.ID(), item.Type); err != nil {
			return nil, fmt.Errorf("failed to create field(s)")
		}
	}

	if err := t.Commit(); err != nil {
		return nil, err
	}

	return db.ContentTypeGet(space, contenttype.ID())
}

func (db *DB) ContentTypesPerSpace(space space.Space, page int) ([]contenttype.ContentType, error) {
	var ret []contenttype.ContentType
	rows, err := db.Query(queryFindContentTypesBySpace, space.ID(), perPage, perPage*page)

M internal/s/db/db.go => internal/s/db/db.go +4 -2
@@ 323,11 323,13 @@ func (db *DB) createTables() []error {
// value types.
func (db *DB) EnsureSetup() error {
	for _, err := range db.createTables() {
		if err != nil && strings.Contains(err.Error(), "Table ") && strings.Contains(err.Error(), "already exists") {
		errmsg := err.Error()

		if err != nil && strings.Contains(errmsg, "Table ") && strings.Contains(errmsg, "already exists") {
			continue
		}

		if err != nil && strings.Contains(err.Error(), "Duplicate entry ") && strings.Contains(err.Error(), "cms_valuetype.VALUE") {
		if err != nil && strings.Contains(errmsg, "Duplicate entry ") && strings.Contains(errmsg, "VALUE") {
			continue
		}


M internal/s/db/db_test.go => internal/s/db/db_test.go +11 -1
@@ 125,7 125,7 @@ func TestBasic(t *testing.T) {
	assert.Equal(t, nil, err)
	isdeleted(t, space, ct2, c4)

	// Fetch a contnet that still exists and was referenced.
	// Fetch a content that still exists and was referenced.
	c5, err := conn.ContentGet(space, ct1, c2.ID())
	assert.Equal(t, nil, err)
	assert.Equal(t, ct1.ID(), c5.Type())


@@ 133,6 133,16 @@ func TestBasic(t *testing.T) {
	assert.Equal(t, "content2", c5.MustValueByName("name").Value())
	assert.Equal(t, "content-2", c5.MustValueByName("slug").Value())
	assert.Equal(t, "long-desc-2", c5.MustValueByName("desc").Value())

	err = conn.ContentTypeDelete(space, ct1)
	assert.Equal(t, nil, err)

	err = conn.SpaceDelete(space)
	assert.Equal(t, nil, err)

	// Now, make sure we space's CTs are deleted
	_, err = conn.ContentTypeGet(space, ct2.ID())
	assert.NotEqual(t, nil, err)
}

func isdeleted(t *testing.T, s space.Space, ct contenttype.ContentType, c content.Content) {

M internal/s/tmpl/css/main.css => internal/s/tmpl/css/main.css +18 -8
@@ 1,12 1,22 @@
body > * {
input { 
  box-sizing: content-box;
  display: block;
}

.tox.tox-tinymce {
  min-height: 600px;
}

body { 
  max-width: 1000px;
  margin-left: auto;
  margin-right: auto;
}

header h1 { 
  max-width: 700px;
  margin-left: auto;
  margin-right: auto;
  text-align: center;
body.space input { 
  display: initial;
}

form[action='/contenttype/new'] input,
form[action='/contenttype/update'] input { 
  display: initial;
}


M internal/s/tmpl/html/_header.html => internal/s/tmpl/html/_header.html +2 -0
@@ 17,5 17,7 @@
      <li><a href="//git.sr.ht/~evanj/cms">Source</a></li>
    </ul>
  </nav>
  <!--
  <h1>A <u>minimalist</u> content management infrastructure for <mark>most</mark>.</h1>
  -->
</header>

M internal/s/tmpl/html/content.html => internal/s/tmpl/html/content.html +1 -1
@@ 6,7 6,7 @@
  <title>CMS | {{ .Space.Name }} | {{ .ContentType.Name }} | {{ (.Content.MustValueByName "name").Value }}</title>
</head>

<body>
<body class=content>
  <style>{{ template "css/main.css" }}</style>

  <main>

M internal/s/tmpl/html/contenttype.html => internal/s/tmpl/html/contenttype.html +55 -1
@@ 6,7 6,7 @@
  <title>CMS | {{ .Space.Name }} | {{ .ContentType.Name }}</title>
</head>

<body>
<body class=contenttype>
  <style>{{ template "css/main.css" }}</style>

  <main>


@@ 94,6 94,59 @@
      </details>

      <details>
        <summary>Update {{ .ContentType.Name }} Content Type</summary>
        <form method=POST action='/contenttype/update' enctype='multipart/form-data'>
          <input required type=hidden name=space value="{{ .Space.ID }}" />
          <input required type=hidden name=contenttype value="{{ .ContentType.ID }}" />
          <input required type=text name=name placeholder="content type name" value="{{ .ContentType.Name }}" />
          <legend>Fields</legend>

          {{ range $index, $item := .ContentType.Fields }}

            {{ if eq $index 0 }}
              <div id='first-fieldset'>
                <input required type=hidden name="field_update_id_{{ inc $index }}" value="{{ .ID }}" />
                <input readonly="readonly" required type=text name="field_update_name_{{ inc $index }}" value="{{ .Name }}" />
                <select value="{{ .Type }}" readonly="readonly" required name="field_update_type_{{ inc $index }}">
                  <option disabled value>Field Type</option>
                  <option selected value="StringSmall">String Small</option>
                  <option disabled value="StringBig">String Big</option>
                  <option disabled value="InputHTML">HTML</option>
                  <option disabled value="InputMarkdown">Markdown</option>
                  <option disabled value="File">File</option>
                  <option disabled value="Date">Date</option>
                  <option disabled value="Reference">Reference</option>
                  <option disabled value="ReferenceList">ReferenceList</option>
                </select>
                <input disabled type=button value='Remove Field' />
              </div>
            {{ else }}
              <div>
                <input required type=hidden name="field_update_id_{{ inc $index }}" value="{{ .ID }}" />
                <input required type=text name="field_update_name_{{ inc $index }}" value="{{ .Name }}" />
                <select value="{{ .Type }}" readonly="readonly" required name="field_update_type_{{ inc $index }}">
                  <option disabled value>Field Type</option>
                  <option {{ if eq .Type "StringSmall" }}   selected {{ else }} disabled {{ end }} value="StringSmall">String Small</option>
                  <option {{ if eq .Type "StringBig" }}     selected {{ else }} disabled {{ end }} value="StringBig">String Big</option>
                  <option {{ if eq .Type "InputHTML" }}     selected {{ else }} disabled {{ end }} value="InputHTML">HTML</option>
                  <option {{ if eq .Type "InputMarkdown" }} selected {{ else }} disabled {{ end }} value="InputMarkdown">Markdown</option>
                  <option {{ if eq .Type "File" }}          selected {{ else }} disabled {{ end }} value="File">File</option>
                  <option {{ if eq .Type "Date" }}          selected {{ else }} disabled {{ end }} value="Date">Date</option>
                  <option {{ if eq .Type "Reference" }}     selected {{ else }} disabled {{ end }} value="Reference">Reference</option>
                  <option {{ if eq .Type "ReferenceList" }} selected {{ else }} disabled {{ end }} value="ReferenceList">ReferenceList</option>
                </select>
                <input type=button value='Remove Field' />
              </div>
            {{ end }}

          {{ end }}

          <input type=button id='add-fieldbtn' value='Add Another Field' />
          <input type=submit value=Update />
        </form>
      </details>

      <details>
        <summary>Delete {{ .ContentType.Name }} Content Type</summary>
        <form method=POST action='/contenttype/delete' enctype='multipart/form-data'>
          <input required type=hidden name=space value="{{ .Space.ID }}" />


@@ 123,6 176,7 @@
  </main>
  <script src='//unpkg.com/tinymce@5.2.0/tinymce.min.js'></script>
  <script src='//unpkg.com/autocomplete.js@0.37.1/dist/autocomplete.min.js'></script>
  <script>{{ template "js/space.js" }}</script>
  <script>{{ template "js/content.js" $ }}</script>
</body>


M internal/s/tmpl/html/hook.html => internal/s/tmpl/html/hook.html +1 -1
@@ 6,7 6,7 @@
  <title>CMS | {{ .Space.Name }} | {{ .Hook.URL }}</title>
</head>

<body>
<body class=hook>
  <style>{{ template "css/main.css" }}</style>

  <main>

M internal/s/tmpl/html/index.html => internal/s/tmpl/html/index.html +1 -1
@@ 6,7 6,7 @@
  <title>CMS</title>
</head>

<body>
<body class=index>
  <style>{{ template "css/main.css" }}</style>

  <main>

M internal/s/tmpl/html/space.html => internal/s/tmpl/html/space.html +1 -1
@@ 6,7 6,7 @@
  <title>CMS | {{ .Space.Name }}</title>
</head>

<body>
<body class=space>
  <style>{{ template "css/main.css" }}</style>

  <main>

M internal/s/tmpl/js/space.js => internal/s/tmpl/js/space.js +13 -0
@@ 34,3 34,16 @@
    })
  })
})();

// For update: remove old fields
(function() { 
  var btns = document.querySelectorAll("form div input[type=button]");
  for (var e = 0; e < btns.length; e++) {
    (function(btn) {
      console.log(btn)
      btn.addEventListener("click", function handelClick() { 
        btn.parentElement.parentElement.removeChild(btn.parentElement);
      });
    })(btns[e]);
  }
})();

M internal/s/tmpl/tmpl.go => internal/s/tmpl/tmpl.go +8 -1
@@ 10,10 10,17 @@ var all *template.Template

func MustParse(name string) *template.Template {
	if all == nil {

		fns := template.FuncMap{
			"inc": func(i int) int { return i + 1 },
		}

		all = template.New("cms")
		for key, val := range tmpls {
			all = template.Must(all.New(key).Parse(val))
			all = template.Must(all.New(key).Funcs(fns).Parse(val))
		}

	}

	return all.Lookup(name)
}

M internal/s/tmpl/tmpls_embed.go => internal/s/tmpl/tmpls_embed.go +92 -13
@@ 7,18 7,28 @@ var tmpls map[string]string
func init() {
	tmpls = make(map[string]string)

	tmpls["css/main.css"] = `body > * {
	tmpls["css/main.css"] = `input { 
  box-sizing: content-box;
  display: block;
}

.tox.tox-tinymce {
  min-height: 600px;
}

body { 
  max-width: 1000px;
  margin-left: auto;
  margin-right: auto;
}

header h1 { 
  max-width: 700px;
  margin-left: auto;
  margin-right: auto;
  text-align: center;
body.space input { 
  display: initial;
}

form[action='/contenttype/new'] input,
form[action='/contenttype/update'] input { 
  display: initial;
}

`

	tmpls["css/mvp.css"] = `:root {


@@ 402,7 412,9 @@ blockquote footer {
      <li><a href="//git.sr.ht/~evanj/cms">Source</a></li>
    </ul>
  </nav>
  <!--
  <h1>A <u>minimalist</u> content management infrastructure for <mark>most</mark>.</h1>
  -->
</header>
`



@@ 414,7 426,7 @@ blockquote footer {
  <title>CMS | {{ .Space.Name }} | {{ .ContentType.Name }} | {{ (.Content.MustValueByName "name").Value }}</title>
</head>

<body>
<body class=content>
  <style>{{ template "css/main.css" }}</style>

  <main>


@@ 531,7 543,7 @@ blockquote footer {
  <title>CMS | {{ .Space.Name }} | {{ .ContentType.Name }}</title>
</head>

<body>
<body class=contenttype>
  <style>{{ template "css/main.css" }}</style>

  <main>


@@ 619,6 631,59 @@ blockquote footer {
      </details>

      <details>
        <summary>Update {{ .ContentType.Name }} Content Type</summary>
        <form method=POST action='/contenttype/update' enctype='multipart/form-data'>
          <input required type=hidden name=space value="{{ .Space.ID }}" />
          <input required type=hidden name=contenttype value="{{ .ContentType.ID }}" />
          <input required type=text name=name placeholder="content type name" value="{{ .ContentType.Name }}" />
          <legend>Fields</legend>

          {{ range $index, $item := .ContentType.Fields }}

            {{ if eq $index 0 }}
              <div id='first-fieldset'>
                <input required type=hidden name="field_update_id_{{ inc $index }}" value="{{ .ID }}" />
                <input readonly="readonly" required type=text name="field_update_name_{{ inc $index }}" value="{{ .Name }}" />
                <select value="{{ .Type }}" readonly="readonly" required name="field_update_type_{{ inc $index }}">
                  <option disabled value>Field Type</option>
                  <option selected value="StringSmall">String Small</option>
                  <option disabled value="StringBig">String Big</option>
                  <option disabled value="InputHTML">HTML</option>
                  <option disabled value="InputMarkdown">Markdown</option>
                  <option disabled value="File">File</option>
                  <option disabled value="Date">Date</option>
                  <option disabled value="Reference">Reference</option>
                  <option disabled value="ReferenceList">ReferenceList</option>
                </select>
                <input disabled type=button value='Remove Field' />
              </div>
            {{ else }}
              <div>
                <input required type=hidden name="field_update_id_{{ inc $index }}" value="{{ .ID }}" />
                <input required type=text name="field_update_name_{{ inc $index }}" value="{{ .Name }}" />
                <select value="{{ .Type }}" readonly="readonly" required name="field_update_type_{{ inc $index }}">
                  <option disabled value>Field Type</option>
                  <option {{ if eq .Type "StringSmall" }}   selected {{ else }} disabled {{ end }} value="StringSmall">String Small</option>
                  <option {{ if eq .Type "StringBig" }}     selected {{ else }} disabled {{ end }} value="StringBig">String Big</option>
                  <option {{ if eq .Type "InputHTML" }}     selected {{ else }} disabled {{ end }} value="InputHTML">HTML</option>
                  <option {{ if eq .Type "InputMarkdown" }} selected {{ else }} disabled {{ end }} value="InputMarkdown">Markdown</option>
                  <option {{ if eq .Type "File" }}          selected {{ else }} disabled {{ end }} value="File">File</option>
                  <option {{ if eq .Type "Date" }}          selected {{ else }} disabled {{ end }} value="Date">Date</option>
                  <option {{ if eq .Type "Reference" }}     selected {{ else }} disabled {{ end }} value="Reference">Reference</option>
                  <option {{ if eq .Type "ReferenceList" }} selected {{ else }} disabled {{ end }} value="ReferenceList">ReferenceList</option>
                </select>
                <input type=button value='Remove Field' />
              </div>
            {{ end }}

          {{ end }}

          <input type=button id='add-fieldbtn' value='Add Another Field' />
          <input type=submit value=Update />
        </form>
      </details>

      <details>
        <summary>Delete {{ .ContentType.Name }} Content Type</summary>
        <form method=POST action='/contenttype/delete' enctype='multipart/form-data'>
          <input required type=hidden name=space value="{{ .Space.ID }}" />


@@ 648,6 713,7 @@ blockquote footer {
  </main>
  <script src='//unpkg.com/tinymce@5.2.0/tinymce.min.js'></script>
  <script src='//unpkg.com/autocomplete.js@0.37.1/dist/autocomplete.min.js'></script>
  <script>{{ template "js/space.js" }}</script>
  <script>{{ template "js/content.js" $ }}</script>
</body>



@@ 662,7 728,7 @@ blockquote footer {
  <title>CMS | {{ .Space.Name }} | {{ .Hook.URL }}</title>
</head>

<body>
<body class=hook>
  <style>{{ template "css/main.css" }}</style>

  <main>


@@ 698,7 764,7 @@ blockquote footer {
  <title>CMS</title>
</head>

<body>
<body class=index>
  <style>{{ template "css/main.css" }}</style>

  <main>


@@ 777,7 843,7 @@ blockquote footer {
  <title>CMS | {{ .Space.Name }}</title>
</head>

<body>
<body class=space>
  <style>{{ template "css/main.css" }}</style>

  <main>


@@ 1116,6 1182,19 @@ blockquote footer {
    })
  })
})();

// For update: remove old fields
(function() { 
  var btns = document.querySelectorAll("form div input[type=button]");
  for (var e = 0; e < btns.length; e++) {
    (function(btn) {
      console.log(btn)
      btn.addEventListener("click", function handelClick() { 
        btn.parentElement.parentElement.removeChild(btn.parentElement);
      });
    })(btns[e]);
  }
})();
`

}