Merge branch 'redesign' into 'master'

Redesign pastebin.run

See merge request pastebin.run/server!91
This commit is contained in:
Konrad Borowski 2019-12-21 17:29:39 +00:00
commit 3fcf4b29e0
17 changed files with 172 additions and 86 deletions

View File

@ -2,6 +2,7 @@ export interface EditorType {
setLanguage(identifier: string): void
getValue(): string
setValue(text: string): void
update(): void
unload(): void
}

View File

@ -1,5 +1,5 @@
.CodeMirror {
border: 1px solid #bbbdbe;
height: 372px;
height: 100%;
font-size: 14.4px;
}

View File

@ -55,6 +55,8 @@ class CodeMirrorEditor {
this.editor.setValue(value)
}
update() {}
unload() {
this.editor.toTextArea()
}

View File

@ -1,6 +1,5 @@
.monaco {
border: 1px solid #bbbdbe;
height: auto;
height: 100%;
font-size: 14.4px;
height: 372px;
}

View File

@ -75,6 +75,14 @@ class MonacoEditor {
this.editor.setValue(value)
}
update() {
// Monaco has no idea how to reflow, so let's force it to reflow twice
this.container.style.width = '0'
this.editor.layout()
this.container.style.width = ''
this.editor.layout()
}
unload() {
this.textarea.value = this.getValue()
this.editor.dispose()

View File

@ -17,6 +17,8 @@ class TextAreaEditor {
this.textarea.value = value
}
update() {}
unload() {
this.textarea.removeEventListener('input', this.onChange)
}

View File

@ -10,14 +10,13 @@ class Editor {
codeElement: HTMLTextAreaElement
output: Output
autodeleteText: HTMLSpanElement
autodeleteCheckbox: HTMLLabelElement
helloWorldLink: HTMLSpanElement
submit: HTMLInputElement
submitButtons: HTMLInputElement[]
detailsElement: HTMLDetailsElement
stdinElement: HTMLTextAreaElement
editor: EditorType
currentLanguage: string | null = null
abortEval: AbortController | null = null
isHelloWorld: boolean = false
initialize(form) {
this.languageSelector = form.querySelector('#language')
@ -26,20 +25,20 @@ class Editor {
this.initializeEditor(createTextareaEditor)
onChange(editor => this.changeEditor(editor))
this.initConfiguredEditor()
this.output = new Output(form.querySelector('#output'))
this.output = Output.addTo(form.querySelector('#split'))
const stdout = document.querySelector<HTMLInputElement>('#dbstdout')
if (stdout) {
this.output.display({}, {
this.displayOutput({}, {
stdout: stdout.value,
stderr: document.querySelector<HTMLInputElement>('#dbstderr').value,
status: +document.querySelector<HTMLInputElement>('#dbstatus') ?.value,
})
}
this.autodeleteText = form.querySelector('#autodelete-text')
this.autodeleteCheckbox = form.querySelector('#automatically-hidden-label')
this.helloWorldLink = form.querySelector('#hello-world')
this.submit = form.querySelector('[type=submit]')
this.submit.disabled = true
this.submitButtons = form.querySelectorAll('[type=submit]')
for (const submit of this.submitButtons) {
submit.disabled = true
}
form.addEventListener('submit', () => {
if (this.output.json && !this.output.wrapper.isFormatter) {
for (const name of ['stdout', 'stderr', 'status']) {
@ -58,7 +57,10 @@ class Editor {
summary.textContent = 'Standard input'
this.stdinElement = document.createElement('textarea')
this.stdinElement.name = 'stdin'
this.stdinElement.addEventListener('change', () => this.changeToLookLikeNewPaste())
this.stdinElement.addEventListener('change', () => {
this.isHelloWorld = false
this.changeToLookLikeNewPaste()
})
this.detailsElement.append(summary, this.stdinElement)
const dbStdin = document.querySelector<HTMLInputElement>('#dbstdin') ?.value
if (dbStdin) {
@ -67,12 +69,10 @@ class Editor {
} else {
this.detailsElement.style.display = 'none'
}
form.querySelector('#buttons').append(this.detailsElement)
if (this.autodeleteText) {
this.autodeleteCheckbox.style.display = 'none'
}
form.querySelector('#extrafields').append(this.detailsElement)
this.assignEvents()
this.updateLanguage()
addEventListener('resize', () => this.editor.update())
}
async initConfiguredEditor() {
@ -85,7 +85,10 @@ class Editor {
}
initializeEditor(createEditor) {
this.editor = createEditor(this.codeElement, () => this.changeToLookLikeNewPaste())
this.editor = createEditor(this.codeElement, () => {
this.changeToLookLikeNewPaste()
this.isHelloWorld = false
})
if (this.currentLanguage) {
this.editor.setLanguage(this.currentLanguage)
}
@ -99,10 +102,10 @@ class Editor {
changeToLookLikeNewPaste() {
if (this.autodeleteText) {
this.autodeleteText.style.display = 'none'
this.autodeleteCheckbox.style.display = ''
}
this.submit.disabled = false
this.output.clear()
for (const submit of this.submitButtons) {
submit.disabled = false
}
}
assignEvents() {
@ -114,21 +117,22 @@ class Editor {
async updateLanguage() {
this.wrapperButtons.clear()
this.helloWorldLink.textContent = ''
const identifier = this.getLanguageIdentifier()
this.setLanguage(identifier)
const isStillValid = () => identifier === this.getLanguageIdentifier()
const language = await getLanguage(identifier, isStillValid)
// This deals with user changing the language after asynchronous event
if (isStillValid()) {
if (language.helloWorldPaste) {
const anchor = document.createElement('a')
anchor.href = '/' + language.helloWorldPaste
anchor.textContent = 'Hello world program'
this.helloWorldLink.append(' | ', anchor)
}
this.detailsElement.style.display = language.implementations.length ? 'block' : 'none'
this.wrapperButtons.update(language.implementations)
const isStillHelloWorld = () => this.isHelloWorld || this.editor.getValue() === ''
if (isStillHelloWorld()) {
const helloWorldText = language.helloWorldPaste ? await (await fetch(`/${language.helloWorldPaste}.txt`)).text() : ""
if (isStillHelloWorld()) {
this.editor.setValue(helloWorldText)
this.isHelloWorld = true
}
}
}
}
@ -138,6 +142,7 @@ class Editor {
async run(wrapper, compilerOptions) {
this.output.clear()
this.editor.update()
if (this.abortEval) {
this.abortEval.abort()
}
@ -168,7 +173,12 @@ class Editor {
if (wrapper.isFormatter) {
this.editor.setValue(response.stdout)
}
this.displayOutput(wrapper, response)
}
displayOutput(wrapper, response) {
this.output.display(wrapper, response)
this.editor.update()
}
}

View File

@ -1,15 +1,29 @@
import { Wrapper } from './types'
import { SplitChunksPlugin } from 'webpack'
const filterRegex = /(?:\t\.(?:text|file|section|globl|p2align|type|cfi_.*|size|section)\b|.Lfunc_end).*\n?/g
export default class Output {
split: HTMLDivElement
outputContainer: HTMLDivElement
output: HTMLDivElement
filterAsm = document.createElement('label')
filterAsmCheckbox = document.createElement('input')
wrapper: Wrapper | null = null
json: { stdout: string, stderr: string, status: number | null } | null = null
constructor(output) {
static addTo(split: HTMLDivElement) {
const outputContainer = document.createElement('div')
outputContainer.id = 'outputcontainer'
const output = document.createElement('div')
output.id = 'output'
outputContainer.append(output)
return new Output(split, outputContainer, output)
}
private constructor(split: HTMLDivElement, outputContainer: HTMLDivElement, output: HTMLDivElement) {
this.split = split
this.outputContainer = outputContainer
this.output = output
this.filterAsmCheckbox.type = 'checkbox'
this.filterAsmCheckbox.checked = true
@ -18,8 +32,8 @@ export default class Output {
}
clear() {
this.json = null
this.output.textContent = ''
this.outputContainer.remove()
}
error() {
@ -34,7 +48,8 @@ export default class Output {
update() {
const { stdout, stderr, status } = this.json
this.output.textContent = ''
this.clear()
this.split.append(this.outputContainer)
if (stderr) {
const stderrHeader = document.createElement('h2')
stderrHeader.textContent = 'Standard error'

View File

@ -29,12 +29,13 @@ export default class WrapperButtons {
this.select.append(option)
}
this.buttonsContainer.textContent = ''
if (implementations.length > 1) {
this.buttonsContainer.append(this.select)
this.select.addEventListener('change', () => this.updateButtons())
}
if (implementations.length !== 0) {
this.buttonsContainer.append(this.compilerOptions, this.buttons)
this.buttonsContainer.append(this.buttons)
if (implementations.length > 1) {
this.buttonsContainer.append(this.select)
this.select.addEventListener('change', () => this.updateButtons())
}
this.buttonsContainer.append(this.compilerOptions)
}
this.updateButtons()
}
@ -71,7 +72,7 @@ export default class WrapperButtons {
first = false
}
button.addEventListener('click', event)
this.buttons.append(button)
this.buttons.append(button, ' ')
}
}
}

View File

@ -112,6 +112,7 @@ pub fn insert(
Ok(insert_paste.identifier)
}
#[derive(Default)]
pub struct ExternPaste {
pub paste: String,
pub language_id: i32,

View File

@ -4,7 +4,6 @@ use crate::Connection;
use chrono::{Duration, Utc};
use futures::Future;
use futures03::TryFutureExt;
use serde::de::IgnoredAny;
use serde::Deserialize;
use tokio_executor::blocking;
use warp::http::header::LOCATION;
@ -15,7 +14,7 @@ use warp::{reply, Rejection, Reply};
pub struct PasteForm {
language: String,
code: String,
autodelete: Option<IgnoredAny>,
share: Share,
#[serde(default)]
stdin: String,
stdout: Option<String>,
@ -23,11 +22,18 @@ pub struct PasteForm {
status: Option<i32>,
}
#[derive(Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Share {
Share,
Share24,
}
pub fn insert_paste(
PasteForm {
language,
code,
autodelete,
share,
stdin,
stdout,
stderr,
@ -36,7 +42,10 @@ pub fn insert_paste(
connection: Connection,
) -> impl Future<Item = impl Reply, Error = Rejection> {
blocking::run(move || {
let delete_at = autodelete.map(|_| Utc::now() + Duration::hours(24));
let delete_at = match share {
Share::Share => None,
Share::Share24 => Some(Utc::now() + Duration::hours(24)),
};
let identifier = paste::insert(
&connection,
delete_at,

View File

@ -203,8 +203,8 @@ fn not_found(pool: PgPool) -> impl Clone + Fn(Rejection) -> NotFoundFuture {
Err(rejection)
}
}
.boxed()
.compat()
.boxed()
.compat()
}
}
@ -310,7 +310,7 @@ mod test {
#[test]
#[cfg_attr(not(feature = "database_tests"), ignore)]
fn test_raw_pastes() {
let body = format!("language={}&code=abc", get_sh_id());
let body = format!("language={}&code=abc&share=share24", get_sh_id());
let reply = warp::test::request()
.method("POST")
.header(CONTENT_LENGTH, body.len())

View File

@ -1,5 +1,11 @@
* {
box-sizing: border-box;
}
body {
margin: 0;
height: 100vh;
display: flex;
flex-direction: column;
line-height: 1.6;
color: black;
text-align: justify;
@ -13,9 +19,7 @@ body {
box-shadow: 0 -5px 2px rgba(0, 0, 0, 0.1) inset;
}
#header div {
margin: 0 auto;
padding: 0 1em;
max-width: 900px;
display: flex;
align-items: center;
justify-content: space-between;
@ -40,17 +44,41 @@ body {
border: none;
}
#article {
margin: 0 auto;
padding: 0 1em;
max-width: 980px;
display: flex;
flex: 1;
flex-direction: column;
padding: 0 1em 1em;
position: relative;
}
form {
display: flex;
flex: 1;
flex-direction: column;
}
h1, h2, h3, h4, h5, h6 {
line-height: 1.2;
}
#toolbar {
padding: 6px 0;
}
#split {
display: flex;
flex: 1;
position: relative;
}
#extrafieldsplit {
display: flex;
flex: 1;
flex-direction: column;
}
#textarea {
flex: 1;
position: relative;
}
textarea {
width: 100%;
height: 372px;
resize: vertical;
height: 100%;
resize: none;
}
[name=stdin] {
height: 124px;
@ -58,6 +86,28 @@ textarea {
#right-buttons {
float: right;
}
#outputcontainer {
flex: 1;
position: relative;
}
#output {
padding: 0 1em;
position: absolute;
width: 100%;
height: 100%;
overflow: auto;
}
@media (max-width: 1200px) {
#split {
flex-direction: column;
flex: auto;
}
#output {
padding: 0;
max-height: 40vh;
max-width: unset;
}
}
table {
overflow: auto;
border-collapse: collapse;

View File

@ -1,8 +1,6 @@
@()
<div id="buttons">
<div id="right-buttons">
<span id="wrapper-buttons"></span>
<input type=submit value=Share>
</div>
</div>
<div id="output"></div>
<span id="wrapper-buttons"></span>
<span id="right-buttons">
<button type=submit name=share value=share24>Share (delete after 24 hours)</button>
<button type=submit name=share value=share>Share</button>
</span>

View File

@ -8,20 +8,21 @@
@Html(paste.markdown)
<form method="post" action="/" id="editor">
<p>
@if let Some(delete_at) = paste.delete_at {
<div id="autodelete-text">
This paste will be automatically deleted on @delete_at.format("%Y-%m-%d %H:%M") UTC.
</div>
}
<div id="toolbar">
@:language_selection(selection)
@if let Some(delete_at) = paste.delete_at {
<span id="autodelete-text">
This paste will be automatically deleted on @delete_at.format("%Y-%m-%d %H:%M") UTC.
</span>
}
<span id="automatically-hidden-label">
<label><input type=checkbox name=autodelete checked> Automatically delete after 24 hours</label>
<span id=hello-world></span>
</span>
</p>
<p><textarea id=code name=code>@('\n')@paste.paste</textarea></p>
@:buttons()
@:buttons()
</div>
<div id="split">
<div id="extrafieldsplit">
<div id="textarea"><textarea id=code name=code>@('\n')@paste.paste</textarea></div>
<div id="extrafields"></div>
</div>
</div>
</form>
<input type=hidden id=dbstdin value="@paste.stdin">
@if let Some(exit_code) = paste.exit_code {

View File

@ -6,7 +6,7 @@
<meta charset=utf-8>
<meta name=viewport content="width=device-width, initial-scale=1">
<title>pastebin.run</title>
<link rel=stylesheet href="/static/style.css">
<link rel=stylesheet href="/static/style-v2.css">
<meta name=description content="@session.description">
<header id=header>
<div>

View File

@ -1,19 +1,8 @@
@use crate::models::language::Selection;
@use crate::models::session::Session;
@use crate::templates::{buttons, header, footer, language_selection};
@use crate::models::paste::ExternPaste;
@use crate::templates::display_paste;
@(session: &Session, selection: Selection)
@:header(session)
<form method="post" action="/" id="editor">
<p>
@:language_selection(selection)
<label>
<input type=checkbox name=autodelete checked> Automatically delete after 24 hours
</label>
<span id=hello-world></span>
</p>
<p><textarea id=code name=code></textarea></p>
@:buttons()
</form>
@:footer(session)
@:display_paste(session, ExternPaste::default(), selection)