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