github.com/rpdict/ponzu@v0.10.1-0.20190226054626-477f29d6bf5e/management/editor/editor.go (about) 1 // Package editor enables users to create edit views from their content 2 // structs so that admins can manage content 3 package editor 4 5 import ( 6 "bytes" 7 "log" 8 "net/http" 9 ) 10 11 // Editable ensures data is editable 12 type Editable interface { 13 MarshalEditor() ([]byte, error) 14 } 15 16 // Mergeable allows external post content to be approved and published through 17 // the public-facing API 18 type Mergeable interface { 19 // Approve copies an external post to the internal collection and triggers 20 // a re-sort of its content type posts 21 Approve(http.ResponseWriter, *http.Request) error 22 } 23 24 // Editor is a view containing fields to manage content 25 type Editor struct { 26 ViewBuf *bytes.Buffer 27 } 28 29 // Field is used to create the editable view for a field 30 // within a particular content struct 31 type Field struct { 32 View []byte 33 } 34 35 // Form takes editable content and any number of Field funcs to describe the edit 36 // page for any content struct added by a user 37 func Form(post Editable, fields ...Field) ([]byte, error) { 38 editor := &Editor{} 39 40 editor.ViewBuf = &bytes.Buffer{} 41 _, err := editor.ViewBuf.WriteString(`<table><tbody class="row"><tr class="col s8 editor-fields"><td class="col s12">`) 42 if err != nil { 43 log.Println("Error writing HTML string to editor Form buffer") 44 return nil, err 45 } 46 47 for _, f := range fields { 48 addFieldToEditorView(editor, f) 49 } 50 51 _, err = editor.ViewBuf.WriteString(`</td></tr>`) 52 if err != nil { 53 log.Println("Error writing HTML string to editor Form buffer") 54 return nil, err 55 } 56 57 // content items with Item embedded have some default fields we need to render 58 _, err = editor.ViewBuf.WriteString(`<tr class="col s4 default-fields"><td class="col s12">`) 59 if err != nil { 60 log.Println("Error writing HTML string to editor Form buffer") 61 return nil, err 62 } 63 64 publishTime := ` 65 <div class="row content-only __ponzu"> 66 <div class="input-field col s6"> 67 <label class="active">MM</label> 68 <select class="month __ponzu browser-default"> 69 <option value="1">Jan - 01</option> 70 <option value="2">Feb - 02</option> 71 <option value="3">Mar - 03</option> 72 <option value="4">Apr - 04</option> 73 <option value="5">May - 05</option> 74 <option value="6">Jun - 06</option> 75 <option value="7">Jul - 07</option> 76 <option value="8">Aug - 08</option> 77 <option value="9">Sep - 09</option> 78 <option value="10">Oct - 10</option> 79 <option value="11">Nov - 11</option> 80 <option value="12">Dec - 12</option> 81 </select> 82 </div> 83 <div class="input-field col s2"> 84 <label class="active">DD</label> 85 <input value="" class="day __ponzu" maxlength="2" type="text" placeholder="DD" /> 86 </div> 87 <div class="input-field col s4"> 88 <label class="active">YYYY</label> 89 <input value="" class="year __ponzu" maxlength="4" type="text" placeholder="YYYY" /> 90 </div> 91 </div> 92 93 <div class="row content-only __ponzu"> 94 <div class="input-field col s3"> 95 <label class="active">HH</label> 96 <input value="" class="hour __ponzu" maxlength="2" type="text" placeholder="HH" /> 97 </div> 98 <div class="col s1">:</div> 99 <div class="input-field col s3"> 100 <label class="active">MM</label> 101 <input value="" class="minute __ponzu" maxlength="2" type="text" placeholder="MM" /> 102 </div> 103 <div class="input-field col s4"> 104 <label class="active">Period</label> 105 <select class="period __ponzu browser-default"> 106 <option value="AM">AM</option> 107 <option value="PM">PM</option> 108 </select> 109 </div> 110 </div> 111 ` 112 113 _, err = editor.ViewBuf.WriteString(publishTime) 114 if err != nil { 115 log.Println("Error writing HTML string to editor Form buffer") 116 return nil, err 117 } 118 119 err = addPostDefaultFieldsToEditorView(post, editor) 120 if err != nil { 121 return nil, err 122 } 123 124 submit := ` 125 <div class="input-field post-controls"> 126 <button class="right waves-effect waves-light btn green save-post" type="submit">Save</button> 127 <button class="right waves-effect waves-light btn red delete-post" type="submit">Delete</button> 128 </div> 129 ` 130 _, ok := post.(Mergeable) 131 if ok { 132 submit += 133 ` 134 <div class="row external post-controls"> 135 <div class="col s12 input-field"> 136 <button class="right waves-effect waves-light btn blue approve-post" type="submit">Approve</button> 137 <button class="right waves-effect waves-light btn grey darken-2 reject-post" type="submit">Reject</button> 138 </div> 139 <label class="approve-details right-align col s12">This content is pending approval. By clicking 'Approve', it will be immediately published. By clicking 'Reject', it will be deleted.</label> 140 </div> 141 ` 142 } 143 144 script := ` 145 <script> 146 $(function() { 147 var form = $('form'), 148 save = form.find('button.save-post'), 149 del = form.find('button.delete-post'), 150 external = form.find('.post-controls.external'), 151 id = form.find('input[name=id]'), 152 timestamp = $('.__ponzu.content-only'), 153 slug = $('input[name=slug]'); 154 155 // hide if this is a new post, or a non-post editor page 156 if (id.val() === '-1' || form.attr('action') !== '/admin/edit') { 157 del.hide(); 158 external.hide(); 159 } 160 161 // hide approval if not on a pending content item 162 if (getParam('status') !== 'pending') { 163 external.hide(); 164 } 165 166 // no timestamp, slug visible on addons 167 if (form.attr('action') === '/admin/addon') { 168 timestamp.hide(); 169 slug.parent().hide(); 170 } 171 172 save.on('click', function(e) { 173 e.preventDefault(); 174 175 if (getParam('status') === 'pending') { 176 var action = form.attr('action'); 177 form.attr('action', action + '?status=pending') 178 } 179 180 form.submit(); 181 }); 182 183 del.on('click', function(e) { 184 e.preventDefault(); 185 var action = form.attr('action'); 186 action = action + '/delete'; 187 form.attr('action', action); 188 189 if (confirm("[Ponzu] Please confirm:\n\nAre you sure you want to delete this post?\nThis cannot be undone.")) { 190 form.submit(); 191 } 192 }); 193 194 external.find('button.approve-post').on('click', function(e) { 195 e.preventDefault(); 196 var action = form.attr('action'); 197 action = action + '/approve'; 198 form.attr('action', action); 199 200 form.submit(); 201 }); 202 203 external.find('button.reject-post').on('click', function(e) { 204 e.preventDefault(); 205 var action = form.attr('action'); 206 action = action + '/delete?reject=true'; 207 form.attr('action', action); 208 209 if (confirm("[Ponzu] Please confirm:\n\nAre you sure you want to reject this post?\nDoing so will delete it, and cannot be undone.")) { 210 form.submit(); 211 } 212 }); 213 }); 214 </script> 215 ` 216 _, err = editor.ViewBuf.WriteString(submit + script + `</td></tr></tbody></table>`) 217 if err != nil { 218 log.Println("Error writing HTML string to editor Form buffer") 219 return nil, err 220 } 221 222 return editor.ViewBuf.Bytes(), nil 223 } 224 225 func addFieldToEditorView(e *Editor, f Field) error { 226 _, err := e.ViewBuf.Write(f.View) 227 if err != nil { 228 log.Println("Error writing field view to editor view buffer") 229 return err 230 } 231 232 return nil 233 } 234 235 func addPostDefaultFieldsToEditorView(p Editable, e *Editor) error { 236 defaults := []Field{ 237 { 238 View: Input("Slug", p, map[string]string{ 239 "label": "URL Slug", 240 "type": "text", 241 "disabled": "true", 242 "placeholder": "Will be set automatically", 243 }), 244 }, 245 { 246 View: Timestamp("Timestamp", p, map[string]string{ 247 "type": "hidden", 248 "class": "timestamp __ponzu", 249 }), 250 }, 251 { 252 View: Timestamp("Updated", p, map[string]string{ 253 "type": "hidden", 254 "class": "updated __ponzu", 255 }), 256 }, 257 } 258 259 for _, f := range defaults { 260 err := addFieldToEditorView(e, f) 261 if err != nil { 262 return err 263 } 264 } 265 266 return nil 267 }