first commit
This is the first commit. With stuff on several lines.
This commit is contained in:
commit
899087ed49
11 changed files with 1101 additions and 0 deletions
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
.make.*
|
||||||
|
dist/code-editor.mjs
|
||||||
|
dist/think-editor.js
|
||||||
|
elm-stuff
|
||||||
|
jj/status
|
||||||
|
log
|
||||||
|
error.txt
|
2
.watchmakerc
Normal file
2
.watchmakerc
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
extensions:
|
||||||
|
- .elm
|
20
Makefile
Normal file
20
Makefile
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
DIRNAME=$(notdir $(CURDIR))
|
||||||
|
|
||||||
|
PROD_DIR=/srv/think.somethingorotherwhatever.com/thinks/static/thinks
|
||||||
|
|
||||||
|
ELMS=$(wildcard src/*.elm)
|
||||||
|
|
||||||
|
deploy: $(PROD_DIR)/think-editor.js $(PROD_DIR)/load-think-editor.mjs $(PROD_DIR)/think-editor.css
|
||||||
|
|
||||||
|
build: dist/think-editor.js
|
||||||
|
|
||||||
|
dist/think-editor.js: src/App.elm $(ELMS)
|
||||||
|
-elm make $< --output=$@ 2> error.txt
|
||||||
|
@cat error.txt
|
||||||
|
|
||||||
|
$(PROD_DIR)/%: dist/%
|
||||||
|
cp $< $@
|
||||||
|
|
||||||
|
upload: app.js index.html style.css
|
||||||
|
rsync -avz . clpland:~/domains/somethingorotherwhatever.com/html/$(DIRNAME)
|
||||||
|
@echo "Uploaded to https://somethingorotherwhatever.com/$(DIRNAME)"
|
1
README.txt
Normal file
1
README.txt
Normal file
|
@ -0,0 +1 @@
|
||||||
|
The Elm app for the think editor.
|
26
dist/load-think-editor.mjs
vendored
Normal file
26
dist/load-think-editor.mjs
vendored
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
import './code-editor.mjs';
|
||||||
|
|
||||||
|
export default async function init_app() {
|
||||||
|
const flags = JSON.parse(document.getElementById('think-editor-data').textContent);
|
||||||
|
const packages = await (await fetch('https://elm-package-list.think.somethingorotherwhatever.com/elm-packages.json')).json();
|
||||||
|
flags.elm_packages = packages;
|
||||||
|
flags.csrf_token = document.getElementById('csrftoken')?.textContent || '';
|
||||||
|
const app = Elm.App.init({node: document.body, flags});
|
||||||
|
|
||||||
|
app.ports.reload_preview.subscribe(() => {
|
||||||
|
console.log('reload preview');
|
||||||
|
const iframe = document.getElementById('preview-frame');
|
||||||
|
if(iframe) {
|
||||||
|
const src = iframe.src;
|
||||||
|
iframe.src = "";
|
||||||
|
setTimeout(() => {
|
||||||
|
iframe.src = src;
|
||||||
|
},10);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
app.ports.show_modal.subscribe(id => {
|
||||||
|
console.log(id);
|
||||||
|
document.getElementById(id).showModal()
|
||||||
|
})
|
||||||
|
}
|
243
dist/think-editor.css
vendored
Normal file
243
dist/think-editor.css
vendored
Normal file
|
@ -0,0 +1,243 @@
|
||||||
|
:root {
|
||||||
|
--spacing: 1em;
|
||||||
|
--half-spacing: calc(0.5 * var(--spacing));
|
||||||
|
--double-spacing: calc(2 * var(--spacing));
|
||||||
|
|
||||||
|
--editor-size: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
color-scheme: light dark;
|
||||||
|
font-family: sans-serif;
|
||||||
|
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: auto 1fr;
|
||||||
|
min-height: 100vh;
|
||||||
|
margin: 0;
|
||||||
|
padding: var(--half-spacing);
|
||||||
|
|
||||||
|
& > header {
|
||||||
|
padding: var(--spacing);
|
||||||
|
& h1 {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
body {
|
||||||
|
background: black;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
& #think-controls {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#editor-size-input + output {
|
||||||
|
width: 5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-path {
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.think-editor {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--spacing);
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
--col-1-width: auto;
|
||||||
|
grid-template:
|
||||||
|
"nav editor preview" min-content
|
||||||
|
"log editor preview" 1fr
|
||||||
|
/ var(--col-1-width) var(--editor-size) var(--preview-size)
|
||||||
|
;
|
||||||
|
|
||||||
|
&:has(#main-nav[open], #log[open]) {
|
||||||
|
--col-1-width: 20em;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > #main-nav {
|
||||||
|
grid-area: nav;
|
||||||
|
}
|
||||||
|
& > #log {
|
||||||
|
grid-area: log;
|
||||||
|
width: 100%;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
& .dragging {
|
||||||
|
background: red;
|
||||||
|
}
|
||||||
|
|
||||||
|
& summary {
|
||||||
|
background: #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > #main-nav > nav {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing);
|
||||||
|
|
||||||
|
& #file-tree {
|
||||||
|
margin: 0;
|
||||||
|
overflow: auto;
|
||||||
|
flex-grow: 1;
|
||||||
|
|
||||||
|
& > li {
|
||||||
|
margin-top: var(--half-spacing);
|
||||||
|
|
||||||
|
& > a {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& .dir {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
& .dir + .file {
|
||||||
|
margin-top: var(--spacing);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& form {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 6em;
|
||||||
|
align-content: start;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
& #make-log {
|
||||||
|
& > pre {
|
||||||
|
max-width: 20em;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& #editor {
|
||||||
|
overflow-x: hidden;
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-basis: var(--editor-size);
|
||||||
|
max-height: 85vh;
|
||||||
|
grid-area: editor;
|
||||||
|
|
||||||
|
& #editor-controls {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing);
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
& > details {
|
||||||
|
text-align: right;
|
||||||
|
|
||||||
|
& > summary {
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
& button {
|
||||||
|
margin: var(--half-spacing) 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
background: white;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
& #code-editor {
|
||||||
|
display: block;
|
||||||
|
max-width: 50vw;
|
||||||
|
padding-bottom: 10em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& #preview {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
grid-area: preview;
|
||||||
|
|
||||||
|
&[open] {
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-shrink: 1;
|
||||||
|
flex-basis: calc(100% - var(--editor-size));
|
||||||
|
}
|
||||||
|
|
||||||
|
& > summary {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > iframe {
|
||||||
|
width: 100%;
|
||||||
|
height: calc(100% - 3em);
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[closed] > iframe {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#file-form {
|
||||||
|
overflow: auto;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
@media (max-width: 100ch) {
|
||||||
|
html {
|
||||||
|
font-size: min(3vw, 16px);
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
grid-template-columns: calc(100svw - var(--spacing));
|
||||||
|
}
|
||||||
|
.think-editor {
|
||||||
|
overflow: visible;
|
||||||
|
grid-template:
|
||||||
|
"nav"
|
||||||
|
"log"
|
||||||
|
"editor"
|
||||||
|
"preview"
|
||||||
|
;
|
||||||
|
|
||||||
|
& > * ~ * {
|
||||||
|
border-top: medium solid #888;
|
||||||
|
margin-top: var(--spacing);
|
||||||
|
}
|
||||||
|
|
||||||
|
& nav {
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
& form {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
& #file-tree {
|
||||||
|
max-height: 7em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& #editor {
|
||||||
|
overflow: auto;
|
||||||
|
& #code-editor {
|
||||||
|
max-width: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& #preview {
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
30
elm.json
Normal file
30
elm.json
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
{
|
||||||
|
"type": "application",
|
||||||
|
"source-directories": [
|
||||||
|
"src"
|
||||||
|
],
|
||||||
|
"elm-version": "0.19.1",
|
||||||
|
"dependencies": {
|
||||||
|
"direct": {
|
||||||
|
"elm/browser": "1.0.2",
|
||||||
|
"elm/core": "1.0.5",
|
||||||
|
"elm/file": "1.0.5",
|
||||||
|
"elm/html": "1.0.0",
|
||||||
|
"elm/http": "2.0.0",
|
||||||
|
"elm/json": "1.1.3",
|
||||||
|
"elm/url": "1.0.0",
|
||||||
|
"elm-community/json-extra": "4.3.0"
|
||||||
|
},
|
||||||
|
"indirect": {
|
||||||
|
"elm/bytes": "1.0.8",
|
||||||
|
"elm/parser": "1.1.0",
|
||||||
|
"elm/time": "1.0.0",
|
||||||
|
"elm/virtual-dom": "1.0.3",
|
||||||
|
"rtfeldman/elm-iso8601-date-strings": "1.1.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"test-dependencies": {
|
||||||
|
"direct": {},
|
||||||
|
"indirect": {}
|
||||||
|
}
|
||||||
|
}
|
39
index.html
Normal file
39
index.html
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
<!DOCTYPE HTML>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||||
|
<title>Elm app by clp</title>
|
||||||
|
<link rel="stylesheet" href="dist/think-editor.css">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<p>Think editor test page</p>
|
||||||
|
<p>It's loading!</p>
|
||||||
|
|
||||||
|
<script id="think-editor-data" type="application/json">
|
||||||
|
{
|
||||||
|
"preview_url": "https://somethingorotherwhatever.com/tiny-elvis/",
|
||||||
|
"slug": "loaded-thing",
|
||||||
|
"files": [
|
||||||
|
{"path": ".", "is_dir": true, "name": ".."},
|
||||||
|
{"path": "src/poo.elm", "is_dir": false, "name": "poo.elm"}
|
||||||
|
],
|
||||||
|
"file_path": "src/poo.elm",
|
||||||
|
"is_dir": false,
|
||||||
|
"file_content": "this\nis\nmy\nfile",
|
||||||
|
"csrf_token": "arg",
|
||||||
|
"elm_packages": [
|
||||||
|
{
|
||||||
|
"name": "poo",
|
||||||
|
"summary": "poop",
|
||||||
|
"version": "1",
|
||||||
|
"license": "p"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<script src="dist/think-editor.js"></script>
|
||||||
|
<script src="test.mjs" type="module"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
21
show-error.mjs
Normal file
21
show-error.mjs
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
export default fetch('/error.txt').then(r=>{
|
||||||
|
if(r.ok) {
|
||||||
|
return r.text();
|
||||||
|
} else {
|
||||||
|
throw('');
|
||||||
|
}
|
||||||
|
}).then(text => {
|
||||||
|
if(!text) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
document.body.innerHTML = '';
|
||||||
|
const error_show = document.createElement('pre');
|
||||||
|
error_show.setAttribute('id','build-error');
|
||||||
|
error_show.style.background = 'black';
|
||||||
|
error_show.style.color = 'white';
|
||||||
|
error_show.style.padding = '1em';
|
||||||
|
error_show.style['font-size'] = '16px';
|
||||||
|
error_show.textContent = text;
|
||||||
|
document.body.appendChild(error_show);
|
||||||
|
return true;
|
||||||
|
}).catch(e => false);
|
702
src/App.elm
Normal file
702
src/App.elm
Normal file
|
@ -0,0 +1,702 @@
|
||||||
|
port module App exposing (..)
|
||||||
|
|
||||||
|
import Browser
|
||||||
|
import Browser.Navigation
|
||||||
|
import File exposing (File)
|
||||||
|
import Html as H exposing (Html)
|
||||||
|
import Html.Attributes as HA
|
||||||
|
import Html.Events as HE
|
||||||
|
import Http exposing (Error(..))
|
||||||
|
import Json.Decode as JD
|
||||||
|
import Json.Decode.Extra exposing (andMap)
|
||||||
|
import Json.Encode as JE
|
||||||
|
import Process
|
||||||
|
import Task
|
||||||
|
import Url.Builder as UB
|
||||||
|
|
||||||
|
port reload_preview : () -> Cmd msg
|
||||||
|
|
||||||
|
port show_modal : String -> Cmd msg
|
||||||
|
|
||||||
|
delayMsg : Float -> msg -> Cmd msg
|
||||||
|
delayMsg delay msg =
|
||||||
|
Task.perform (always msg) (Process.sleep delay)
|
||||||
|
|
||||||
|
main = Browser.document
|
||||||
|
{ init = init
|
||||||
|
, update = update
|
||||||
|
, subscriptions = subscriptions
|
||||||
|
, view = view
|
||||||
|
}
|
||||||
|
|
||||||
|
type alias FileInfo =
|
||||||
|
{ name : String
|
||||||
|
, is_dir : Bool
|
||||||
|
, path : String
|
||||||
|
}
|
||||||
|
|
||||||
|
type alias ElmPackage =
|
||||||
|
{ name : String
|
||||||
|
, summary : String
|
||||||
|
, version : String
|
||||||
|
, license : String
|
||||||
|
}
|
||||||
|
|
||||||
|
decode_elm_package =
|
||||||
|
JD.map4
|
||||||
|
ElmPackage
|
||||||
|
(JD.field "name" JD.string)
|
||||||
|
(JD.field "summary" JD.string)
|
||||||
|
(JD.field "version" JD.string)
|
||||||
|
(JD.field "license" JD.string)
|
||||||
|
|
||||||
|
file_info name = { name = name, path = name, is_dir = False }
|
||||||
|
dir_info name = { name = name, path = name, is_dir = True }
|
||||||
|
|
||||||
|
file_edit_url f = (UB.relative [] [UB.string "path" f.path])
|
||||||
|
|
||||||
|
type Log
|
||||||
|
= NotLoaded
|
||||||
|
| HttpError Http.Error
|
||||||
|
| MakeResult String String
|
||||||
|
| MakeError String
|
||||||
|
| CommandResult String String
|
||||||
|
|
||||||
|
type alias CommandResult = Result String { stdout : String, stderr : String }
|
||||||
|
|
||||||
|
type alias Model =
|
||||||
|
{ show_preview : Bool
|
||||||
|
, show_log : Bool
|
||||||
|
, editor_size : Float
|
||||||
|
, content_changed : Bool
|
||||||
|
, log : Log
|
||||||
|
, command_to_run : String
|
||||||
|
, selected_package : String
|
||||||
|
, jj_status : String
|
||||||
|
, commit_message : String
|
||||||
|
|
||||||
|
, csrf_token : String
|
||||||
|
, preview_url : String
|
||||||
|
, slug : String
|
||||||
|
, files : List FileInfo
|
||||||
|
, file_path : String
|
||||||
|
, file_content : String
|
||||||
|
, is_dir : Bool
|
||||||
|
, elm_packages : List ElmPackage
|
||||||
|
}
|
||||||
|
|
||||||
|
init_model : Model
|
||||||
|
init_model =
|
||||||
|
{ show_preview = True
|
||||||
|
, show_log = False
|
||||||
|
, editor_size = 0.5
|
||||||
|
, content_changed = False
|
||||||
|
, log = NotLoaded
|
||||||
|
, command_to_run = ""
|
||||||
|
, selected_package = ""
|
||||||
|
, jj_status = ""
|
||||||
|
, commit_message = ""
|
||||||
|
|
||||||
|
, csrf_token = ""
|
||||||
|
, preview_url = ""
|
||||||
|
, slug = "not-a-real-thing"
|
||||||
|
, files = []
|
||||||
|
, file_path = "Oops!"
|
||||||
|
, file_content = "The editor has not loaded successfully."
|
||||||
|
, is_dir = False
|
||||||
|
, elm_packages = []
|
||||||
|
}
|
||||||
|
|
||||||
|
type DropFileAction
|
||||||
|
= Upload
|
||||||
|
| Content
|
||||||
|
|
||||||
|
type Msg
|
||||||
|
= SetFileContent String
|
||||||
|
| SaveContent String
|
||||||
|
| FileSaved String (Result Http.Error CommandResult)
|
||||||
|
| SetLog (Result Http.Error String)
|
||||||
|
| ReloadLog
|
||||||
|
| ReloadPreview
|
||||||
|
| SetEditorSize Float
|
||||||
|
| TogglePreview Bool
|
||||||
|
| ToggleLog Bool
|
||||||
|
| DropFiles DropFileAction (List File)
|
||||||
|
| NoOp
|
||||||
|
| UploadFile File String
|
||||||
|
| FileUploaded File
|
||||||
|
| SetCommand String
|
||||||
|
| RunCommand String
|
||||||
|
| ReceiveCommandResult (Result Http.Error CommandResult)
|
||||||
|
| SelectPackage String
|
||||||
|
| ShowCommitModal
|
||||||
|
| ReceiveJJStatus String
|
||||||
|
| SetCommitMessage String
|
||||||
|
| JJCommit
|
||||||
|
| JJCommitResponse (Result Http.Error CommandResult)
|
||||||
|
|
||||||
|
init : JE.Value -> (Model, Cmd Msg)
|
||||||
|
init flags = (load_flags flags, reload_log)
|
||||||
|
|
||||||
|
load_flags =
|
||||||
|
JD.decodeValue
|
||||||
|
( JD.succeed
|
||||||
|
(Model
|
||||||
|
init_model.show_preview
|
||||||
|
init_model.show_log
|
||||||
|
init_model.editor_size
|
||||||
|
init_model.content_changed
|
||||||
|
init_model.log
|
||||||
|
init_model.command_to_run
|
||||||
|
init_model.selected_package
|
||||||
|
init_model.jj_status
|
||||||
|
init_model.commit_message
|
||||||
|
)
|
||||||
|
|> andMap (JD.field "csrf_token" JD.string)
|
||||||
|
|> andMap (JD.field "preview_url" JD.string)
|
||||||
|
|> andMap (JD.field "slug" JD.string)
|
||||||
|
|> andMap (JD.field "files" (JD.list decode_file_info))
|
||||||
|
|> andMap (JD.field "file_path" JD.string)
|
||||||
|
|> andMap (JD.field "file_content" JD.string)
|
||||||
|
|> andMap (JD.field "is_dir" JD.bool)
|
||||||
|
|> andMap (JD.oneOf [JD.field "elm_packages" (JD.list decode_elm_package), JD.succeed []])
|
||||||
|
)
|
||||||
|
>> Result.withDefault init_model
|
||||||
|
|
||||||
|
decode_file_info =
|
||||||
|
JD.map3
|
||||||
|
FileInfo
|
||||||
|
(JD.field "name" JD.string)
|
||||||
|
(JD.field "is_dir" JD.bool)
|
||||||
|
(JD.field "path" JD.string)
|
||||||
|
|
||||||
|
decode_command_response =aq
|
||||||
|
JD.oneOf
|
||||||
|
[ JD.field "error" JD.string |> JD.map Err
|
||||||
|
, JD.map2 (\stdout stderr -> Ok { stdout = stdout, stderr = stderr })
|
||||||
|
(JD.field "stdout" JD.string)
|
||||||
|
(JD.field "stderr" JD.string)
|
||||||
|
]
|
||||||
|
|
||||||
|
set_log : Log -> Model -> Model
|
||||||
|
set_log log model = { model | log = log, show_log = True }
|
||||||
|
|
||||||
|
nocmd model = (model, Cmd.none)
|
||||||
|
|
||||||
|
update : Msg -> Model -> (Model, Cmd Msg)
|
||||||
|
update msg model = case msg of
|
||||||
|
SetFileContent content -> ({ model | file_content = content, content_changed = True }, delayMsg 1000 (SaveContent content))
|
||||||
|
|
||||||
|
SaveContent content ->
|
||||||
|
if content == model.file_content then
|
||||||
|
( model
|
||||||
|
, Http.post
|
||||||
|
{ url = "save-file"
|
||||||
|
, body = Http.multipartBody
|
||||||
|
[ Http.stringPart "path" model.file_path
|
||||||
|
, Http.stringPart "content" model.file_content
|
||||||
|
, Http.stringPart "csrfmiddlewaretoken" model.csrf_token
|
||||||
|
]
|
||||||
|
, expect = Http.expectJson (FileSaved content) decode_command_response
|
||||||
|
}
|
||||||
|
)
|
||||||
|
else
|
||||||
|
(model, Cmd.none)
|
||||||
|
|
||||||
|
FileSaved content response -> case response of
|
||||||
|
Ok (Ok res) -> ({ model | content_changed = content /= model.file_content, log = MakeResult res.stdout res.stderr }, delayMsg 1 ReloadPreview)
|
||||||
|
Ok (Err errmsg) -> ({ model | content_changed = content /= model.file_content, log = MakeError errmsg }, delayMsg 1 ReloadPreview)
|
||||||
|
Err err -> { model | log = HttpError err } |> nocmd
|
||||||
|
|
||||||
|
ReloadLog ->
|
||||||
|
(model
|
||||||
|
, reload_log
|
||||||
|
)
|
||||||
|
|
||||||
|
SetLog response -> case response of
|
||||||
|
Ok log ->
|
||||||
|
{ model | log = (MakeResult log "") } |> nocmd
|
||||||
|
Err errmsg -> model |> set_log (HttpError errmsg) |> nocmd
|
||||||
|
|
||||||
|
ReloadPreview -> (model, reload_preview ())
|
||||||
|
|
||||||
|
SetEditorSize size -> { model | editor_size = size } |> nocmd
|
||||||
|
|
||||||
|
TogglePreview show -> { model | show_preview = show } |> nocmd
|
||||||
|
|
||||||
|
ToggleLog show -> { model | show_log = show } |> nocmd
|
||||||
|
|
||||||
|
DropFiles action files -> case List.head files of
|
||||||
|
Just file -> case action of
|
||||||
|
Content -> (model, Task.perform SetFileContent (File.toString file))
|
||||||
|
Upload -> (model, Task.perform (UploadFile file) (File.toString file))
|
||||||
|
Nothing -> (model, Cmd.none)
|
||||||
|
|
||||||
|
UploadFile file contents ->
|
||||||
|
( model
|
||||||
|
, Http.post
|
||||||
|
{ url = "save-file"
|
||||||
|
, body = Http.multipartBody
|
||||||
|
[ Http.stringPart "path" (File.name file)
|
||||||
|
, Http.stringPart "content" contents
|
||||||
|
, Http.stringPart "csrfmiddlewaretoken" model.csrf_token
|
||||||
|
]
|
||||||
|
, expect = Http.expectWhatever
|
||||||
|
(\r -> case r of
|
||||||
|
Ok _ -> FileUploaded file
|
||||||
|
Err _ -> NoOp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
FileUploaded file ->
|
||||||
|
let
|
||||||
|
name = File.name file
|
||||||
|
url = file_edit_url { name = name, path = name, is_dir = False }
|
||||||
|
in
|
||||||
|
(model, Browser.Navigation.load url)
|
||||||
|
|
||||||
|
SetCommand cmd -> { model | command_to_run = cmd } |> nocmd
|
||||||
|
|
||||||
|
RunCommand cmd ->
|
||||||
|
(model
|
||||||
|
, Http.post
|
||||||
|
{ url = "run-command"
|
||||||
|
, body = Http.multipartBody
|
||||||
|
[ Http.stringPart "command" cmd
|
||||||
|
, Http.stringPart "csrfmiddlewaretoken" model.csrf_token
|
||||||
|
]
|
||||||
|
, expect = Http.expectJson ReceiveCommandResult decode_command_response
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
SelectPackage name -> { model | selected_package = name } |> nocmd
|
||||||
|
|
||||||
|
ReceiveCommandResult response -> case response of
|
||||||
|
Ok (Ok res) -> model |> set_log (MakeResult res.stdout res.stderr) |> nocmd
|
||||||
|
Ok (Err errmsg) -> model |> set_log (MakeError errmsg) |> nocmd
|
||||||
|
Err err -> model |> set_log (HttpError err) |> nocmd
|
||||||
|
|
||||||
|
ShowCommitModal -> (model, Cmd.batch [fetch_jj_status, show_modal "commit-modal"])
|
||||||
|
|
||||||
|
SetCommitMessage message -> { model | commit_message = message } |> nocmd
|
||||||
|
|
||||||
|
ReceiveJJStatus status -> { model | jj_status = status, commit_message = "" } |> nocmd
|
||||||
|
|
||||||
|
JJCommit ->
|
||||||
|
( model
|
||||||
|
, Http.post
|
||||||
|
{ url = "jj/commit"
|
||||||
|
, body = Http.multipartBody
|
||||||
|
[ Http.stringPart "message" model.commit_message
|
||||||
|
, Http.stringPart "csrfmiddlewaretoken" model.csrf_token
|
||||||
|
]
|
||||||
|
, expect = Http.expectJson JJCommitResponse
|
||||||
|
decode_command_response
|
||||||
|
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
JJCommitResponse response -> case response of
|
||||||
|
Ok (Ok res) -> { model | log = MakeResult res.stdout res.stderr } |> nocmd
|
||||||
|
Ok (Err errmsg) -> { model | log = MakeError errmsg } |> nocmd
|
||||||
|
Err err -> { model | log = HttpError err } |> nocmd
|
||||||
|
|
||||||
|
NoOp -> (model, Cmd.none)
|
||||||
|
|
||||||
|
reload_log =
|
||||||
|
Http.get
|
||||||
|
{ url = "log"
|
||||||
|
, expect = Http.expectString SetLog
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch_jj_status =
|
||||||
|
Http.get
|
||||||
|
{ url = "jj/status"
|
||||||
|
, expect = Http.expectJson
|
||||||
|
(\r -> case r of
|
||||||
|
Ok response -> ReceiveJJStatus response
|
||||||
|
Err _ -> NoOp
|
||||||
|
)
|
||||||
|
(JD.field "status" JD.string)
|
||||||
|
}
|
||||||
|
|
||||||
|
subscriptions model = Sub.none
|
||||||
|
|
||||||
|
link url text =
|
||||||
|
H.a
|
||||||
|
[ HA.href url]
|
||||||
|
[ H.text text ]
|
||||||
|
|
||||||
|
|
||||||
|
form : Model -> List (H.Attribute Msg) -> List (Html Msg) -> Html Msg
|
||||||
|
form model attrs children =
|
||||||
|
H.form
|
||||||
|
attrs
|
||||||
|
( children
|
||||||
|
++ [ H.input
|
||||||
|
[ HA.type_ "hidden"
|
||||||
|
, HA.name "csrfmiddlewaretoken"
|
||||||
|
, HA.value model.csrf_token
|
||||||
|
]
|
||||||
|
[]
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
as_percentage : Float -> String
|
||||||
|
as_percentage amount = (String.fromInt <| round (100 * amount)) ++ "%"
|
||||||
|
|
||||||
|
view : Model -> Browser.Document Msg
|
||||||
|
view model =
|
||||||
|
{
|
||||||
|
title = model.file_path ++ " - " ++ model.slug ++ " - Thinks",
|
||||||
|
body =
|
||||||
|
[ header model
|
||||||
|
|
||||||
|
, H.main_
|
||||||
|
[ HA.classList
|
||||||
|
[ ("think-editor", True)
|
||||||
|
]
|
||||||
|
, HA.attribute "style" <| ("--editor-size: " ++ (String.fromFloat model.editor_size)++"fr"++";--preview-size: "++(String.fromFloat (1-model.editor_size))++"fr")
|
||||||
|
]
|
||||||
|
[ main_nav model
|
||||||
|
, log_pane model
|
||||||
|
, if model.is_dir then H.text "" else editor_pane model
|
||||||
|
, preview_pane model
|
||||||
|
]
|
||||||
|
|
||||||
|
, commit_modal model
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
header model =
|
||||||
|
H.header
|
||||||
|
[]
|
||||||
|
[ link "/" "thinks"
|
||||||
|
, H.h1
|
||||||
|
[]
|
||||||
|
[ H.text model.slug ]
|
||||||
|
, H.p
|
||||||
|
[ HA.id "think-controls" ]
|
||||||
|
[ H.a
|
||||||
|
[ HA.target "preview"
|
||||||
|
, HA.href model.preview_url
|
||||||
|
]
|
||||||
|
[ H.text "Preview" ]
|
||||||
|
, link "rename" "Rename"
|
||||||
|
, link "delete" "Delete"
|
||||||
|
, link ("/new/"++model.slug) "Remix"
|
||||||
|
, H.label
|
||||||
|
[ HA.for "editor-size-input"
|
||||||
|
]
|
||||||
|
[ H.text "File editor size" ]
|
||||||
|
, H.input
|
||||||
|
[ HA.type_ "range"
|
||||||
|
, HA.id "editor-size-input"
|
||||||
|
, HA.min "0"
|
||||||
|
, HA.max "1"
|
||||||
|
, HA.step "0.05"
|
||||||
|
, HA.value <| String.fromFloat model.editor_size
|
||||||
|
, HE.on "input" (JD.at ["target", "valueAsNumber"] JD.float |> JD.map SetEditorSize)
|
||||||
|
, HA.list "size-values"
|
||||||
|
]
|
||||||
|
[]
|
||||||
|
, H.output
|
||||||
|
[ HA.for "editor-size-input" ]
|
||||||
|
[ H.text <| as_percentage model.editor_size ]
|
||||||
|
, H.datalist
|
||||||
|
[ HA.id "size-values" ]
|
||||||
|
[ H.option [ HA.value "0.5" ] [] ]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
main_nav model =
|
||||||
|
H.details
|
||||||
|
[ HA.attribute "open" ""
|
||||||
|
, HA.id "main-nav"
|
||||||
|
, HE.preventDefaultOn "drop" (JD.at ["dataTransfer", "files"] (JD.list File.decoder) |> JD.map (\f -> (DropFiles Upload f, True)) )
|
||||||
|
, HE.preventDefaultOn "dragover" (JD.succeed (NoOp,True))
|
||||||
|
]
|
||||||
|
[ H.summary
|
||||||
|
[]
|
||||||
|
[ H.text "Files" ]
|
||||||
|
, H.nav
|
||||||
|
[]
|
||||||
|
[ H.ul
|
||||||
|
[ HA.id "file-tree" ]
|
||||||
|
(List.map (\f ->
|
||||||
|
H.li
|
||||||
|
[ HA.classList
|
||||||
|
[ ("dir", f.is_dir)
|
||||||
|
, ("file", not f.is_dir)
|
||||||
|
, ("file-path", True)
|
||||||
|
]
|
||||||
|
]
|
||||||
|
[ link (file_edit_url f) f.name ]
|
||||||
|
) model.files
|
||||||
|
)
|
||||||
|
, form model
|
||||||
|
[ HA.id "file-form"
|
||||||
|
, HA.method "GET"
|
||||||
|
, HA.action "edit"
|
||||||
|
]
|
||||||
|
[ H.input
|
||||||
|
[ HA.attribute "aria-labelledby" "new-file-button"
|
||||||
|
, HA.id "new-file-path"
|
||||||
|
, HA.type_ "text"
|
||||||
|
, HA.name "path"
|
||||||
|
, HA.value <| (if model.is_dir then model.file_path else String.join "/" <| List.reverse <| List.drop 1 <| List.reverse <| String.split "/" model.file_path)++"/"
|
||||||
|
]
|
||||||
|
[]
|
||||||
|
, H.button
|
||||||
|
[ HA.id "new-file-button"
|
||||||
|
, HA.type_ "submit"
|
||||||
|
]
|
||||||
|
[ H.text "New file" ]
|
||||||
|
]
|
||||||
|
, form model
|
||||||
|
[ HE.onSubmit <| RunCommand model.command_to_run
|
||||||
|
]
|
||||||
|
[ H.input
|
||||||
|
[ HA.attribute "aria-labelledby" "run-command-button"
|
||||||
|
, HE.onInput SetCommand
|
||||||
|
, HA.name "command"
|
||||||
|
, HA.value model.command_to_run
|
||||||
|
]
|
||||||
|
[]
|
||||||
|
, H.button
|
||||||
|
[ HA.id "run-command-button"
|
||||||
|
, HA.type_ "submit"
|
||||||
|
]
|
||||||
|
[ H.text "Run" ]
|
||||||
|
]
|
||||||
|
, if String.right 4 model.file_path == ".elm" then
|
||||||
|
form model
|
||||||
|
[ HE.onSubmit <| case model.selected_package of
|
||||||
|
"" -> NoOp
|
||||||
|
p -> RunCommand <| "bash -c \"echo 'Y' | elm install " ++ p ++ "\""
|
||||||
|
]
|
||||||
|
[ H.input
|
||||||
|
[ HA.list "elm-packages"
|
||||||
|
, HE.onInput SelectPackage
|
||||||
|
, HA.value <| model.selected_package
|
||||||
|
]
|
||||||
|
[]
|
||||||
|
, H.node "datalist"
|
||||||
|
[ HA.id "elm-packages"]
|
||||||
|
(List.map (\p -> H.option [HA.value p.name] [H.text p.name]) model.elm_packages)
|
||||||
|
, H.button
|
||||||
|
[ HA.id "install-package-button"
|
||||||
|
, HA.type_ "submit"
|
||||||
|
]
|
||||||
|
[ H.text "Install" ]
|
||||||
|
]
|
||||||
|
else
|
||||||
|
H.text ""
|
||||||
|
, H.button
|
||||||
|
[ HA.id "start-commit-button"
|
||||||
|
, HA.type_ "button"
|
||||||
|
, HE.onClick ShowCommitModal
|
||||||
|
]
|
||||||
|
[ H.text "Commit" ]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
editor_pane model =
|
||||||
|
H.section
|
||||||
|
[ HA.id "editor"
|
||||||
|
, HE.preventDefaultOn "drop" (JD.at ["dataTransfer", "files"] (JD.list File.decoder) |> JD.map (\f -> (DropFiles Content f, True)) )
|
||||||
|
, HE.preventDefaultOn "dragover" (JD.succeed (NoOp,True))
|
||||||
|
]
|
||||||
|
[ H.nav
|
||||||
|
[ HA.id "editor-controls" ]
|
||||||
|
[ H.span
|
||||||
|
[ HA.id "file-path"
|
||||||
|
, HA.class "file-path"
|
||||||
|
]
|
||||||
|
[ H.text model.file_path ]
|
||||||
|
, H.span
|
||||||
|
[ HA.id "file-changed-status"
|
||||||
|
, HA.classList
|
||||||
|
[ ("changed", model.content_changed) ]
|
||||||
|
]
|
||||||
|
[ H.text <| if model.content_changed then "changed" else "saved" ]
|
||||||
|
, H.details
|
||||||
|
[]
|
||||||
|
[ H.summary
|
||||||
|
[]
|
||||||
|
[ H.text "Actions" ]
|
||||||
|
, form model
|
||||||
|
[ HA.method "POST"
|
||||||
|
, HA.action "delete-file"
|
||||||
|
]
|
||||||
|
[ H.input
|
||||||
|
[ HA.type_ "hidden"
|
||||||
|
, HA.name "path"
|
||||||
|
, HA.value model.file_path
|
||||||
|
]
|
||||||
|
[]
|
||||||
|
, H.button
|
||||||
|
[ HA.type_ "submit" ]
|
||||||
|
[ H.text "Delete" ]
|
||||||
|
]
|
||||||
|
, form model
|
||||||
|
[ HA.method "POST"
|
||||||
|
, HA.action "rename-file"
|
||||||
|
]
|
||||||
|
[ H.input
|
||||||
|
[ HA.type_ "hidden"
|
||||||
|
, HA.name "path"
|
||||||
|
, HA.value model.file_path
|
||||||
|
]
|
||||||
|
[]
|
||||||
|
, H.input
|
||||||
|
[ HA.type_ "text"
|
||||||
|
, HA.name "newpath"
|
||||||
|
, HA.value model.file_path
|
||||||
|
]
|
||||||
|
[]
|
||||||
|
, H.button
|
||||||
|
[ HA.type_ "submit" ]
|
||||||
|
[ H.text "Rename" ]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
, form model
|
||||||
|
[ HA.id "file-form"
|
||||||
|
, HA.method "POST"
|
||||||
|
, HA.action "save-file"
|
||||||
|
]
|
||||||
|
[ H.node "code-editor"
|
||||||
|
[ HE.on "change" (JD.at ["target", "value"] JD.string |> JD.map SetFileContent)
|
||||||
|
, HA.attribute "content" model.file_content
|
||||||
|
]
|
||||||
|
[ H.text model.file_content ]
|
||||||
|
, H.input
|
||||||
|
[ HA.name "path"
|
||||||
|
, HA.value model.file_path
|
||||||
|
, HA.type_ "hidden"
|
||||||
|
]
|
||||||
|
[]
|
||||||
|
, H.input
|
||||||
|
[ HA.name "content"
|
||||||
|
, HA.value model.file_content
|
||||||
|
, HA.type_ "hidden"
|
||||||
|
]
|
||||||
|
[]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
log_pane : Model -> Html Msg
|
||||||
|
log_pane model =
|
||||||
|
H.details
|
||||||
|
([HA.id "log"
|
||||||
|
, HE.on "toggle" (JD.at ["target", "open"] JD.bool |> JD.map ToggleLog)
|
||||||
|
]++(if model.show_log then [ HA.attribute "open" ""] else [])
|
||||||
|
)
|
||||||
|
[ H.summary
|
||||||
|
[]
|
||||||
|
[ H.text "Log" ]
|
||||||
|
, case model.log of
|
||||||
|
NotLoaded -> H.text "Not loaded"
|
||||||
|
|
||||||
|
MakeResult stdout stderr ->
|
||||||
|
H.dl
|
||||||
|
[]
|
||||||
|
[ H.dt [] [H.text "stdout" ]
|
||||||
|
, H.dd [] [H.pre [] [ H.text stdout ]]
|
||||||
|
, H.dt [] [H.text "stderr" ]
|
||||||
|
, H.dd [] [H.pre [] [ H.text stderr ]]
|
||||||
|
]
|
||||||
|
|
||||||
|
CommandResult stdout stderr ->
|
||||||
|
H.dl
|
||||||
|
[]
|
||||||
|
[ H.dt [] [H.text "stdout" ]
|
||||||
|
, H.dd [] [H.pre [] [ H.text stdout ]]
|
||||||
|
, H.dt [] [H.text "stderr" ]
|
||||||
|
, H.dd [] [H.pre [] [ H.text stderr ]]
|
||||||
|
]
|
||||||
|
|
||||||
|
MakeError err ->
|
||||||
|
H.div
|
||||||
|
[]
|
||||||
|
[ H.h2 [] [ H.text "Error"]
|
||||||
|
, H.pre [] [H.text err]
|
||||||
|
]
|
||||||
|
|
||||||
|
HttpError err ->
|
||||||
|
let
|
||||||
|
text = case err of
|
||||||
|
BadUrl url -> "Bad URL " ++ url
|
||||||
|
Timeout -> "Timeout"
|
||||||
|
NetworkError -> "Network error"
|
||||||
|
BadStatus code -> "Bad response code " ++ (String.fromInt code)
|
||||||
|
BadBody body -> "Bad body " ++ body
|
||||||
|
in
|
||||||
|
H.div
|
||||||
|
[]
|
||||||
|
[ H.h2 [] [ H.text "Network error"]
|
||||||
|
, H.pre [] [H.text text]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
preview_pane model =
|
||||||
|
H.details
|
||||||
|
[ HA.attribute "open" ""
|
||||||
|
, HA.id "preview"
|
||||||
|
, HE.on "toggle" (JD.at ["target", "open"] JD.bool |> JD.map TogglePreview)
|
||||||
|
]
|
||||||
|
( [ H.summary
|
||||||
|
[]
|
||||||
|
[ H.text "Preview"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
++ if model.show_preview then
|
||||||
|
[ H.iframe
|
||||||
|
[ HA.id "preview-frame"
|
||||||
|
, HA.src model.preview_url
|
||||||
|
]
|
||||||
|
[]
|
||||||
|
]
|
||||||
|
else
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
|
commit_modal model =
|
||||||
|
let
|
||||||
|
view_status status =
|
||||||
|
H.tr
|
||||||
|
[]
|
||||||
|
[ H.td [] [ H.text <| status.path ] ]
|
||||||
|
in
|
||||||
|
H.node "dialog"
|
||||||
|
[ HA.id "commit-modal" ]
|
||||||
|
[ H.h2 [] [H.text "Commit"]
|
||||||
|
, H.pre [ HA.id "jj-status" ] [H.text model.jj_status]
|
||||||
|
, H.form
|
||||||
|
[ HE.on "submit"
|
||||||
|
( JD.at ["submitter","value"] JD.string
|
||||||
|
|> JD.map (\v -> case v of
|
||||||
|
"cancel" -> NoOp
|
||||||
|
_ -> JJCommit
|
||||||
|
)
|
||||||
|
)
|
||||||
|
, HA.method "dialog"
|
||||||
|
]
|
||||||
|
[ H.textarea
|
||||||
|
[ HA.value model.commit_message
|
||||||
|
, HE.onInput SetCommitMessage
|
||||||
|
]
|
||||||
|
[]
|
||||||
|
, H.button
|
||||||
|
[ HA.value "cancel"
|
||||||
|
, HA.attribute "formmethod" "dialog"
|
||||||
|
]
|
||||||
|
[ H.text "Cancel"]
|
||||||
|
, H.button
|
||||||
|
[ HA.type_ "submit"
|
||||||
|
]
|
||||||
|
[ H.text "Commit"]
|
||||||
|
]
|
||||||
|
]
|
10
test.mjs
Normal file
10
test.mjs
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import show_error from './show-error.mjs';
|
||||||
|
import init_app from './dist/load-think-editor.mjs';
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
const compilation_error = await show_error;
|
||||||
|
if(compilation_error) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
init_app();
|
||||||
|
})()
|
Loading…
Reference in a new issue