Initial port to React

feature/core
Sven Slootweg 6 years ago
parent 4dd5bf7d59
commit 2016c0dc44

@ -0,0 +1,82 @@
module.exports = {
"env": {
"browser": true,
"commonjs": true,
"es6": true,
"node": true
},
"parserOptions": {
"ecmaFeatures": {
"experimentalObjectRestSpread": true,
"jsx": true
}
},
"plugins": [
"react",
"react-hooks"
],
"rules": {
/* Things that should effectively be syntax errors. */
"indent": [ "error", "tab", {
SwitchCase: 1
}],
"linebreak-style": [ "error", "unix" ],
"semi": [ "error", "always" ],
/* Things that are always mistakes. */
"getter-return": [ "error" ],
"no-compare-neg-zero": [ "error" ],
"no-dupe-args": [ "error" ],
"no-dupe-keys": [ "error" ],
"no-duplicate-case": [ "error" ],
"no-empty": [ "error" ],
"no-empty-character-class": [ "error" ],
"no-ex-assign": [ "error" ],
"no-extra-semi": [ "error" ],
"no-func-assign": [ "error" ],
"no-invalid-regexp": [ "error" ],
"no-irregular-whitespace": [ "error" ],
"no-obj-calls": [ "error" ],
"no-sparse-arrays": [ "error" ],
"no-undef": [ "error" ],
"no-unreachable": [ "error" ],
"no-unsafe-finally": [ "error" ],
"use-isnan": [ "error" ],
"valid-typeof": [ "error" ],
"curly": [ "error" ],
"no-caller": [ "error" ],
"no-fallthrough": [ "error" ],
"no-extra-bind": [ "error" ],
"no-extra-label": [ "error" ],
"array-callback-return": [ "error" ],
"prefer-promise-reject-errors": [ "error" ],
"no-with": [ "error" ],
"no-useless-concat": [ "error" ],
"no-unused-labels": [ "error" ],
"no-unused-expressions": [ "error" ],
"no-unused-vars": [ "error" , {
argsIgnorePattern: "^_",
varsIgnorePattern: "^_"
} ],
"no-return-assign": [ "error" ],
"no-self-assign": [ "error" ],
"no-new-wrappers": [ "error" ],
"no-redeclare": [ "error" ],
"no-loop-func": [ "error" ],
"no-implicit-globals": [ "error" ],
"strict": [ "error", "global" ],
/* Make JSX not cause 'unused variable' errors. */
"react/jsx-uses-react": ["error"],
"react/jsx-uses-vars": ["error"],
/* Development code that should be removed before deployment. */
"no-console": [ "warn" ],
"no-constant-condition": [ "warn" ],
"no-debugger": [ "warn" ],
"no-alert": [ "warn" ],
"no-warning-comments": ["warn", {
terms: ["fixme"]
}],
/* Common mistakes that can *occasionally* be intentional. */
"no-template-curly-in-string": ["warn"],
"no-unsafe-negation": [ "warn" ],
}
};

@ -0,0 +1,47 @@
"use strict";
const path = require("path");
const app = require("../src/server/app.js")();
if (process.env.NODE_ENV === "development") {
const budo = require("budo");
function projectPath(targetPath) {
return path.resolve(__dirname, "..", targetPath);
}
budo(projectPath("src/client/index.jsx"), {
watchGlob: projectPath("public/css/*.css"),
live: "**/*.css",
stream: process.stdout,
port: 8000,
/* NOTE: If the below is not set to match watchGlob, then livereloads will silently fail */
dir: projectPath("public"),
serve: "js/bundle.js",
debug: true,
browserify: {
extensions: [".jsx"],
plugin: [
"browserify-hmr"
],
transform: [
["babelify", {
presets: ["@babel/preset-env", "@babel/preset-react"],
plugins: ["react-hot-loader/babel"]
}]
]
},
middleware: function (req, res, next) {
app.handle(req, res, (err) => {
if (err != null && err instanceof Error) {
res.send("<pre>" + err.stack + "</pre>");
} else {
next(err);
}
});
}
});
} else {
app.listen(3000);
}

@ -0,0 +1,7 @@
"use strict";
const createError = require("create-error");
module.exports = {
SyntaxError: createError("SyntaxError", {isFatal: false})
};

@ -0,0 +1,25 @@
"use strict";
const errors = require("./errors");
module.exports = function createSyntaxError({line, column, error}) {
if (line == null) {
throw new Error("Must specify a line number");
} else if (column == null) {
throw new Error("Must specify a column number");
} else if (error == null) {
throw new Error("Must specify what caused the syntax error");
} else if (typeof line !== "number") {
throw new Error("Line number must be numeric");
} else if (typeof column !== "number") {
throw new Error("Column number must be numeric");
} else if (typeof error !== "string") {
throw new Error("Syntax error cause must be a string");
} else {
return new errors.SyntaxError("A syntax error occurred", {
line: line,
column: column,
error: error
});
}
};

@ -0,0 +1,52 @@
"use strict";
const Promise = require("bluebird");
const createEventEmitter = require("create-event-emitter");
const defaultValue = require("default-value");
const mergeMetadata = require("./util/merge-metadata");
module.exports = function createFileSink(options) {
let emitter = createEventEmitter({
__buildStep: "sink",
displayName: options.displayName,
supportsAcknowledgment: options.supportsAcknowledgment,
supportsStreams: options.supportsStreams,
sink: function (inputFile) {
let {metadata} = inputFile;
try {
let result = options.sink(inputFile);
if (result != null) {
return Promise.try(() => {
return result;
}).then((extraMetadata = {}) => {
emitter.emit("sunk", {
metadata: mergeMetadata(extraMetadata, {
inputFileMetadata: metadata
}),
stream: inputFile.stream,
contents: inputFile.contents
});
}).catch((err) => {
reportError(inputFile, err);
});
}
} catch (err) {
reportError(inputFile, err);
}
}
});
function reportError(inputFile, error) {
emitter.emit("error", {
file: inputFile,
error: error,
isFatal: error.isFatal,
step: emitter
});
}
return emitter;
};

@ -0,0 +1,55 @@
"use strict";
const createEventEmitter = require("create-event-emitter");
const path = require("path");
const mergeMetadata = require("./util/merge-metadata");
module.exports = function createFileSource(options) {
function resolvePath(relativePath) {
return path.resolve("", relativePath);
}
let emitter = createEventEmitter({
__buildStep: "source",
displayName: options.displayName,
supportsTeardown: options.supportsTeardown,
basePath: options.basePath,
initialize: function (initializationOptions) {
try {
return options.initialize({
options: initializationOptions,
resolvePath: resolvePath,
pushFile: newFile,
reportError: reportError
});
} catch (error) {
reportError(error);
}
},
teardown: options.teardown
});
function newFile({metadata, contents, stream}) {
let augmentedMetadata = mergeMetadata(metadata, {
transform: emitter,
sourcedDate: new Date()
});
emitter.emit("file", {
metadata: augmentedMetadata,
contents: contents,
stream: stream
});
}
function reportError(error, isFatal) {
emitter.emit("error", {
error: error,
isFatal: isFatal,
step: emitter
});
}
return emitter;
};

@ -0,0 +1,13 @@
"use strict";
module.exports = {
defineTask: function (func) {
return func;
}, // callback
pipeline: require("./pipeline"),
watch: require("./watch"),
saveToDirectory: require("./save-to-directory"), // folder path
createFileSource: require("./file-source"),
createFileSink: require("./file-sink"),
createSingleFileTransform: require("./single-file-transform")
};

@ -0,0 +1,104 @@
"use strict";
const path = require("path");
const defaultValue = require("default-value");
const createEventEmitter = require("create-event-emitter");
const mergeMetadata = require("./util/merge-metadata");
module.exports = function pipeline(steps) {
if (steps.length < 2) {
throw new Error("A pipeline must consist of at least two steps");
} else if (steps[0].__buildStep !== "source") {
throw new Error("The first transform in a pipeline must be a file source");
} else if (steps[steps.length - 1].__buildStep !== "sink") {
throw new Error("The last transform in a pipeline must be a file sink");
} else {
let basePath = steps[0].basePath;
let sources = steps.slice(0, -1);
let sinks = steps.slice(1);
let firstSource = sources[0];
let finalSink = sinks[sinks.length - 1];
let emitter = createEventEmitter({
basePath: basePath, /* QUESTION: Should sources be required to provide this? */
steps: steps, /* FIXME: Make private? */
initialize: function () {
let allSinksSupportStreams = sinks.every((sink) => sink.supportsStreams === true);
/* This will start the flow. */
firstSource.initialize({
supportsStreams: allSinksSupportStreams
});
}
});
steps.forEach((step) => {
step.on("error", (data) => {
if (data instanceof Error) {
/* FIXME: This is because any error within an event handler will automatically result in an `error` event. Probably should rename the transform error event to avoid interfering with this. */
process.nextTick(() => {
throw data;
});
} else {
let error = Object.assign({
isFatal: true
}, data);
emitter.emit("error", error);
}
});
});
sources.forEach((source, i) => {
source.on("file", ({metadata, contents, stream}) => {
/* FIXME: Add stream-to-contents conversion? */
let augmentedMetadata;
if (metadata.isVirtual === false && metadata.relativePath == null) {
/* FIXME: Add check that 1) absolute path is set, and 2) it lies within the basePath */
augmentedMetadata = mergeMetadata(metadata, {
relativePath: path.relative(basePath, metadata.path)
});
} else {
augmentedMetadata = metadata;
}
augmentedMetadata.basePath = basePath;
/* NOTE: The below indexes into `steps`, not `sources`, because the last source sends to the final sink. Both `steps` and `sources` arrays start at the same point. */
let nextSink = steps[i + 1];
try {
let sinkInput = {
metadata: augmentedMetadata,
contents: contents,
stream: stream
};
if (nextSink.__buildStep === "transform") {
nextSink.transform(sinkInput);
} else if (nextSink.__buildStep === "sink") {
nextSink.sink(sinkInput);
}
} catch (error) {
emitter.emit("error", {
step: nextSink,
file: augmentedMetadata,
error: error,
isFatal: defaultValue(error.isFatal, true)
});
}
});
});
finalSink.on("sunk", ({metadata}) => {
emitter.emit("done", {
metadata: metadata,
acknowledged: finalSink.supportsAcknowledgment
});
});
return emitter;
}
};

@ -0,0 +1,45 @@
"use strict";
function midBranch(line) {
return `${line}`;
}
function midPipe(line) {
return `${line}`;
}
function destinationBranch(line) {
return ` └🠲 ${line}`;
}
function destinationPipe(line) {
return ` ${line}`;
}
function item(text, branchFunc, pipeFunc) {
let lines = text.split("\n");
let firstLine = branchFunc(lines[0]);
if (lines.length === 1) {
return firstLine;
} else {
let otherLines = lines.slice(1).map((line) => pipeFunc(line));
return [firstLine].concat(otherLines).join("\n");
}
}
module.exports = function drawTransformTree(history) {
let firstItem = history[0];
let lastItem = item(history[history.length - 1], destinationBranch, destinationPipe);
let otherItems;
if (history.length > 2) {
otherItems = history.slice(1, -1).map((historyItem) => {
return item(historyItem, midBranch, midPipe);
});
} else {
otherItems = [];
}
return [firstItem].concat(otherItems).concat([lastItem]).join("\n");
};

@ -0,0 +1,231 @@
"use strict";
const chalk = require("chalk");
const moment = require("moment");
const path = require("path");
const traverseTransformHistory = require("../util/traverse-transform-history");
const drawTransformTree = require("./draw-transform-tree");
const errors = require("../errors/errors");
function formatDate(date) {
return chalk.dim(`[${moment(date).format("LTS")}]`);
}
function formatSuccessfulSourcePath(sourcePath) {
return chalk.bgGreen.black(`${sourcePath} `);
}
function formatFailedSourcePath(sourcePath) {
return chalk.bgRed.white(`${sourcePath} `);
}
function formatDuration(ms) {
return chalk.bold(`${ms}ms`);
}
function formatTransformDisplayName(displayName) {
return displayName;
}
function formatTransformPath(relativePath) {
return chalk.dim(`[${relativePath}]`);
}
function formatSinkPath(sinkPath) {
return chalk.bold(sinkPath);
}
function formatSuccessfulSourceStep(metadata) {
return `${formatSuccessfulSourcePath(metadata.relativePath)} ${formatDate(metadata.sourcedDate)}`;
}
function formatSuccessfulTransformStep(metadata) {
return `${formatDuration(metadata.stepDuration)} ${formatTransformDisplayName(metadata.step.displayName)} ${formatTransformPath(metadata.relativePath)}`;
}
function formatSuccessfulSinkStep(metadata) {
return formatSinkPath(metadata.targetPath);
}
function formatMissingTransformStep(step) {
return chalk.dim(`${chalk.bold("not run:")} ${step.displayName}`);
}
function formatMissingSinkStep() {
return chalk.dim("(no output file produced)");
}
function formatFailedSourceStep(source) {
return `${formatFailedSourcePath(source.relativePath)} ${formatDate(source.sourcedDate)}`;
}
function formatFailedTransformStepName(step, duration) {
return chalk.red(`${formatDuration(duration)} ${step.displayName}`);
}
function formatErrorDetail(name, description) {
return chalk.red(`${chalk.bold(`${name}:`)} ${description}`);
}
function formatFailedTransformStep(step, file, error, pipeline, duration) {
if (error instanceof errors.SyntaxError) {
let prefix = formatErrorPrefix("Syntax Error");
let message = formatErrorMessage(error.error);
return [
formatFailedTransformStepName(step, duration),
`${prefix}${message}`,
formatErrorDetail("File", `${path.resolve(pipeline.basePath, file.metadata.relativePath)} ${file.metadata.isVirtual ? chalk.dim("(virtual)") : ""}`),
formatErrorDetail("Location", `Line ${error.line}, column ${error.column}`),
(file.metadata.isVirtual)
? chalk.bgRed.white.bold("This file is a virtual file. That means that the syntax error probably originates from another earlier transform, and there may be a bug in that transform.")
: ""
].join("\n");
} else {
let prefix = formatErrorPrefix("Error");
let message = formatErrorMessage(`An unexpected error occurred in the transform`);
/* FIXME: */
// if (error instanceof Error) {
// console.log(chalk.red(error.stack));
// } else {
// console.log(chalk.red(util.inspect(error, {colors: true, depth: null})));
// console.log(chalk.bgRed.white.bold("This error wasn't actually an error object - that's a bug in the transform! Here's a less precise stacktrace:"));
// console.log(chalk.red((new Error()).stack));
// }
return [
formatFailedTransformStepName(step),
`${prefix}${message}`,
error.stack
].join("\n");
}
}
// console.log(chalk.bgRed.white.bold(`A syntax error occurred in the transform '${transform.displayName}':`));
// console.log(chalk.red(` ${chalk.bold("Error:")} ${error.error}`));
// console.log(chalk.red(` ${chalk.bold("File:")} ${path.resolve(basePath, file.relativePath)} ${file.isVirtual ? chalk.dim("(virtual)") : ""}`));
// console.log(chalk.red(` ${chalk.bold("Location:")} Line ${error.line}, column ${error.column}`));
function formatFailedSinkStep(step, error) {
let prefix = formatErrorPrefix("Error");
let message = formatErrorMessage(`Failed to write file to destination`);
return [
formatFailedTransformStepName(step),
`${prefix}${message}`,
error.stack
].join("\n");
}
function formatErrorPrefix(text) {
return chalk.bgRed.white(` ${text} `);
}
function formatErrorMessage(message) {
return chalk.bgWhite.black(` ${message} `);
}
function formatCompletedItem(metadata) {
let history = traverseTransformHistory(metadata);
let sourceStep = formatSuccessfulSourceStep(history[0]);
let transformSteps = history.slice(1, -1).map((metadata) => {
return formatSuccessfulTransformStep(metadata);
});
let sinkStep = formatSuccessfulSinkStep(history[history.length - 1]);
return drawTransformTree([sourceStep].concat(transformSteps).concat([sinkStep]));
}
function formatFailedItem(pipeline, {error, step, file, duration}) {
let history = traverseTransformHistory(file.metadata);
let sourceStep = formatFailedSourceStep(history[0]);
let transformSteps, sinkStep;
if (step.__buildStep === "sink") {
transformSteps = history.slice(1, -1).map((metadata) => {
return formatSuccessfulTransformStep(metadata);
});
sinkStep = formatFailedSinkStep(history[history.length - 1], error);
} else {
let failedIndex = pipeline.steps.indexOf(step);
let successfulTransformSteps = history.slice(1, failedIndex).map((metadata) => {
return formatSuccessfulTransformStep(metadata);
});
let failedTransformStep = formatFailedTransformStep(pipeline.steps[failedIndex], file, error, pipeline, duration);
let missingTransformSteps = pipeline.steps.slice(failedIndex + 1, -1).map((step) => {
return formatMissingTransformStep(step);
});
transformSteps = successfulTransformSteps.concat([failedTransformStep]).concat(missingTransformSteps);
sinkStep = formatMissingSinkStep();
}
return drawTransformTree([sourceStep].concat(transformSteps).concat([sinkStep]));
}
module.exports = function runTask(task) {
let pipeline = task();
pipeline.on("error", (failure) => {
console.log(formatFailedItem(pipeline, failure));
if (failure.isFatal) {
console.log(chalk.bgRed.white.bold("This error is fatal, and the build process will now exit."));
/* FIXME: Do this more cleanly, with teardown and all. */
process.exit(1);
}
console.log("");
});
pipeline.on("done", ({metadata, acknowledged}) => {
console.log(formatCompletedItem(metadata));
if (!acknowledged) {
console.log(chalk.gray("(NOTE: The destination sink doesn't support acknowledgments, so the file may not actually be stored yet.)"));
}
console.log("");
});
pipeline.initialize();
};
// function reportError(transform, file, error, isFatal) {
// if (error instanceof errors.SyntaxError) {
// console.log(chalk.bgRed.white.bold(`A syntax error occurred in the transform '${transform.displayName}':`));
// console.log(chalk.red(` ${chalk.bold("Error:")} ${error.error}`));
// console.log(chalk.red(` ${chalk.bold("File:")} ${path.resolve(basePath, file.relativePath)} ${file.isVirtual ? chalk.dim("(virtual)") : ""}`));
// console.log(chalk.red(` ${chalk.bold("Location:")} Line ${error.line}, column ${error.column}`));
//
//
// } else {
// console.log(chalk.bgRed.white.bold(`An error occurred in the transform '${transform.displayName}':`));
//
//
// }
//
// }
/* FIXME: Move this stuff out, use the acknowledgment event instead for detecting which tasks have finished; also don't forget to add a workaround for sinks that cannot acknowledge */
// console.log("===============");
// console.log(metadata);
// console.log(`Wrote ${targetFilePath}`);

@ -0,0 +1,33 @@
"use strict";
const Promise = require("bluebird");
const fs = Promise.promisifyAll(require("fs"));
const mkdirpAsync = Promise.promisify(require("mkdirp"));
const path = require("path");
const createFileSink = require("./file-sink");
module.exports = function (targetPath) {
return createFileSink({
displayName: `Save to directory (${targetPath})`,
supportsAcknowledgment: true,
/* FIXME: Implement stream support */
supportsStreams: false,
sink: function ({metadata, contents}) {
return Promise.try(() => {
/* FIXME: Verify that all writes are within the targetPath */
let targetFilePath = path.join(targetPath, metadata.relativePath);
return Promise.try(() => {
return mkdirpAsync(path.dirname(targetFilePath));
}).then(() => {
return fs.writeFileAsync(targetFilePath, contents);
}).then(() => {
return {
targetPath: targetFilePath
};
});
});
}
});
};

@ -0,0 +1,51 @@
"use strict";
const Promise = require("bluebird");
const createEventEmitter = require("create-event-emitter");
const defaultValue = require("default-value");
const mergeMetadata = require("./util/merge-metadata");
/* FIXME: Associate output files with input files! */
module.exports = function createSingleFileTransform(options) {
let emitter = createEventEmitter({
__buildStep: "transform",
displayName: options.displayName,
supportsStreams: options.supportsStreams,
supportsVirtualFiles: options.supportsVirtualFiles,
supportsVirtualFilesystem: options.supportsVirtualFilesystem,
transform: function (input) {
let startTime = Date.now();
return Promise.try(() => {
return options.transform({
metadata: input.metadata,
contents: input.contents,
stream: input.stream
});
}).then(({metadata, contents, stream}) => {
emitter.emit("file", {
metadata: mergeMetadata(metadata, {
inputFileMetadata: input.metadata,
lastModified: defaultValue(metadata.lastModified, new Date()),
step: this,
stepDuration: Date.now() - startTime
}),
contents: contents,
stream: stream
});
}).catch((error) => {
emitter.emit("error", {
file: input,
step: this,
error: error,
isFatal: error.isFatal,
duration: Date.now() - startTime
});
});
}
});
return emitter;
};

@ -0,0 +1,27 @@
"use strict";
const path = require("path");
let platformDoubleSlashMatcher;
if (path.sep === "\\") {
/* Special case; even within a regex literal, a backslash still needs to be escaped itself. */
platformDoubleSlashMatcher = /\\+/g;
} else {
/* See: https://stackoverflow.com/questions/4547609/how-do-you-get-a-string-to-a-character-array-in-javascript/34717402#34717402 */
let escapedSeparator = Array.from(path.sep).map((character) => `\\${character}`).join("");
platformDoubleSlashMatcher = new RegExp(`(${escapedSeparator})+`, "g");
}
module.exports = function splitPath(targetPath, normalize = true) {
let normalized;
if (normalize) {
normalized = path.normalize(targetPath);
} else {
normalized = targetPath.replace(platformDoubleSlashMatcher, path.sep);
}
return normalized.split(path.sep);
};

@ -0,0 +1,6 @@
"use strict";
/* NOTE: This function will likely grow some more complex merging logic later on. */
module.exports = function mergeMetadata(original, newData) {
return Object.assign({}, original, newData);
};

@ -0,0 +1,9 @@
"use strict";
const path = require("path");
module.exports = function replaceExtension(originalPath, newExtension) {
let parsedPath = path.parse(originalPath);
return path.join(parsedPath.dir, `${parsedPath.name}.${newExtension}`);
};

@ -0,0 +1,13 @@
"use strict";
module.exports = function traverseTransformHistory(metadata) {
let history = [];
let current = metadata;
while (current != null) {
history.push(current);
current = current.inputFileMetadata;
}
return history.reverse();
};

@ -0,0 +1,98 @@
"use strict";
const Promise = require("bluebird");
const chokidar = require("chokidar");
const createFileSource = require("./file-source");
const fs = Promise.promisifyAll(require("fs"));
const path = require("path");
const splitPath = require("./split-path");
/* FIXME: Add deletion propagation support, based on correlating input files to output files */
module.exports = function watch(pattern, options = {}) {
let watcher;
let basePath;
if (options.basePath != null) {
/* FIXME: Verify that, if an explicit basePath is specified, it's not deeper in the path than the automatically-determined one (for any of the specified patterns); otherwise file-paths-relative-to-the-base-path no longer make any sense. */
basePath = options.basePath;
} else {
if (typeof pattern === "string") {
let pathSegments = splitPath(pattern);
let firstWildcardSegment = pathSegments.findIndex((segment) => {
return segment.includes("*");
});
if (firstWildcardSegment === -1) {
/* Assume an exact file path, and treat its folder as the base folder */
basePath = path.dirname(pattern);
} else {
/* Assume a path containing a wildcard, and treat the last non-wildcard segment as the base folder */
basePath = pathSegments.slice(0, firstWildcardSegment).join(path.sep);
}
} else {
throw new Error("When specifying multiple patterns to watch, you must explicitly specify a basePath in the options");
}
}
return createFileSource({
displayName: `Watch for changes (${pattern})`,
supportsTeardown: true,
basePath: basePath,
initialize: ({options, resolvePath, pushFile, reportError}) => {
function push(filePath, stats) {
return Promise.try(() => {
/* FIXME: Define more sensible resolvePath semantics */
let resolvedPath = resolvePath(filePath);
let metadata = {
isVirtual: false,
path: resolvedPath,
lastModified: stats.mtime
};
/* FIXME: File object format validation */
if (options.supportsStreams) {
pushFile({
metadata: {
isStream: true,
... metadata
},
stream: fs.createReadStream(resolvedPath)
});
} else {
return Promise.try(() => {
return fs.readFileAsync(resolvedPath);
}).then((contents) => {
pushFile({
metadata: {
isStream: false,
... metadata
},
contents: contents
});
});
}
}).catch((err) => {
reportError(err, false);
});
}
watcher = chokidar.watch(pattern, {alwaysStat: true})
.on("add", (path, stats) => {
push(path, stats);
})
.on("change", (path, stats) => {
push(path, stats);
})
.on("error", (err) => {
/* FIXME: Determine whether the error is fatal */
reportError(err, false);
});
},
teardown: function () {
watcher.close();
}
});
};

@ -0,0 +1,19 @@
"use strict";
const core = require("./core");
const scssTransform = require("./transforms/scss");
const dummyTransform = require("./transforms/dummy");
const runner = require("./core/runner");
let scssTask = core.defineTask(() => {
return core.pipeline([
core.watch("src/client/scss/**/*.scss"),
dummyTransform(),
scssTransform(),
core.saveToDirectory("public/css")
]);
});
// scssTask();
runner(scssTask);

@ -0,0 +1,79 @@
# TYPES
- event daemon(?) for eg. nodemon
- task
- pipeline
- step (source, transform, sink)
- file
- failure
## Task
Represents a task/job.
Contains:
- Pipeline or event daemon
## Pipeline
Represents a sequence of operations, from source to sink.
Contains:
- Array of steps
## Step
Can be either source, transform, or sink -- a transform conceptually counts as both source and sink, sort of.
Contains:
- __buildStep (type: source, transform, sink)
- displayName
- Source only:
- supportsTeardown
- basePath
- initialize (logic)
- teardown (logic)
- Transform only:
- supportsStreams
- supportsVirtualFiles
- supportsVirtualFilesystem
- transform (logic)
- Sink only:
- supportsStreams
- supportsAcknowledgment
- sink (logic)
## File
A file, real or virtual; equates to a successful event on a source/transform.
Contains:
- Metadata
- Contents or Stream
## Metadata
All the data that describes the file; nests to refer to parent input file's metadata.
Contains:
- inputFileMetadata (only when originating from transform or sink)
- step: The step that this file originates from
- stepDuration: How long the originating step took
- sourcedDate (only when originating from source)
- lastModified (optional)
- targetPath (save-to-directory sink only?)
- relativePath
- basePath
## Failure
Signifies that something went wrong.
Contains:
- error: Error object/data
- step: Step that it originated from
- file: Input file (only for sinks/transforms)
- isFatal
- duration (only for transforms)

@ -0,0 +1,18 @@
"use strict";
const core = require("../core");
module.exports = function () {
return core.createSingleFileTransform({
displayName: "Dummy transform",
// stream: where 'contents' is a stream
supportsStreams: true,
// virtual files: files with a contents buffer/stream but without a path
supportsVirtualFiles: true,
// virtual filesystem: not-on-disk, eg. for includes of transformed files
supportsVirtualFilesystem: true,
transform: (file) => {
return file;
}
});
};

@ -0,0 +1,111 @@
"use strict";
const Promise = require("bluebird");
const path = require("path");
const core = require("../core");
const nodeSass = Promise.promisifyAll(require("node-sass"));
const copyProps = require("copy-props");
const replaceExtension = require("../core/util/replace-extension");
const createSyntaxError = require("../core/errors/syntax-error");
let optionsToCopy = [
"functions",
"indentedSyntax",
"indentType",
"indentWidth",
"linefeed",
"omitSourceMapUrl", // FIXME: outFile stuff
"outputStyle",
"precision",
"sourceComments",
"sourceMap", // FIXME: outFile stuff
"sourceMapContents",
"sourceMapEmbed",
"sourceMapRoot"
];
module.exports = function (options = {}) {
return core.createSingleFileTransform({
displayName: "SCSS (node-sass)",
// stream: where 'contents' is a stream
supportsStreams: false,
// virtual files: files with a contents buffer/stream but without a path
supportsVirtualFiles: true,
// virtual filesystem: not-on-disk, eg. for includes of transformed files
supportsVirtualFilesystem: (options.virtualFilesystemSupport === true),
transform: ({metadata, contents}) => {
let includePaths;
if (!metadata.isVirtual) {
includePaths = [path.dirname(metadata.path)];
} else {
includePaths = [];
}
if (options.includePaths != null) {
includePaths = includePaths.concat(options.includePaths);
}
let importer;
if (options.virtualFilesystemSupport === true && metadata.virtualFilesystem != null) {
importer = function include(target, previousPath, done) {
if (/^[a-z]+:/i.test(target)) {
/* Probably a URL; don't handle this. */
return null;
} else {
return Promise.try(() => {
return metadata.virtualFilesystem.readFile(target);
}).then((contents) => {
done(contents.toString());
}).catch((err) => {
done(err);
});
}
};
}
return Promise.try(() => {
let stringContents = contents.toString();
if (stringContents.length > 0) {
let renderOptions = {
data: stringContents,
includePaths: includePaths,
importer: importer,
};
copyProps(options, renderOptions, optionsToCopy);
return nodeSass.renderAsync(renderOptions);
} else {
return {css: contents};
}
}).then((result) => {
return {
metadata: {
/* FIXME: Implement a check that throws errors when a transform returns isVirtual:false for a non-existent path on a non-virtual FS */
isVirtual: true,
/* FIXME: Verify that every transform returns a relativePath? */
relativePath: replaceExtension(metadata.relativePath, "css")
},
contents: result.css
};
}).catch({status: 1}, (err) => {
/* Syntax error */
throw createSyntaxError({
error: err.message,
line: err.line,
column: err.column
});
}).catch({status: 3}, (err) => {
// no input provided
throw new Error(err.message);
}).catch((err) => {
// console.log(require("util").inspect(err, {colors: true, depth: null}));
throw err;
}); /* FIXME: Error mapping! */
}
});
};

@ -1,109 +0,0 @@
var gulp = require('gulp');
var path = require('path');
var rename = require('gulp-rename');
var livereload = require('gulp-livereload');
var nodemon = require("gulp-nodemon");
var net = require("net");
var webpack = require("webpack-stream-fixed");
var webpackLib = require("webpack")
const presetSCSS = require("@joepie91/gulp-preset-scss");
var nodemonRestarting = false;
function tryReload() {
if (nodemonRestarting === false) {
livereload.changed.apply(null, arguments);
}
}
/* The following resolves JacksonGariety/gulp-nodemon#33 */
process.once("SIGINT", function() {
process.exit(0);
});
gulp.task('webpack', function(){
return gulp.src("./lib/frontend/index.js")
.pipe(webpack({
watch: true,
module: {
preLoaders: [{
test: /\.tag$/,
loader: "riotjs-loader",
exclude: /node_modules/,
query: {
type: "es6",
template: "pug",
/* NOTE: `node-sass` is implicitly required based on the
`type` attribute in the components */
parserOptions: {
js: {
presets: ["es2015-riot"]
}
}
}
}],
loaders: [
{ test: /\.js$/, loader: "babel-loader" },
{ test: /\.json$/, loader: "json-loader" },
//{ loader: "logging-loader" }
]
},
plugins: [ new webpackLib.ProvidePlugin({riot: "riot"}) ],
resolveLoader: { root: path.join(__dirname, "node_modules") },
resolve: {
extensions: [
"",
".tag",
".web.js", ".js",
".web.json", ".json"
]
},
debug: false
}))
.pipe(rename("bundle.js"))
.pipe(gulp.dest("./public/js"));
});
gulp.task("scss", () => {
return gulp.src("./scss/**/*")
.pipe(presetSCSS({
livereload: livereload
}))
.pipe(gulp.dest("./public/stylesheets/"));
});
function checkServerUp(){
setTimeout(function(){
var sock = new net.Socket();
sock.setTimeout(50);
sock.on("connect", function(){
nodemonRestarting = false;
console.log("Triggering page reload...");
tryReload("*");
sock.destroy();
})
.on("timeout", checkServerUp)
.on("error", checkServerUp)
.connect(3000);
}, 70);
}
gulp.task("nodemon", function() {
nodemon({
script: "./app.js",
delay: 500,
ignore: ["public"]
}).on("restart", function() {
nodemonRestarting = true;
}).on("start", checkServerUp);
})
gulp.task('watch', ["nodemon"], function () {
livereload.listen();
gulp.watch(['./**/*.css', 'views/**/*.pug', '!views/client/**/*.pug', 'package.json', "./public/js/**/*.js"]).on('change', tryReload);
gulp.watch(['public/views/**/*.html', 'public/elements/**/*']).on('change', function() { tryReload("*"); }); // We need to explicitly reload everything here; Polymer doesn't do partial reloading
});
gulp.task("default", ["watch", "webpack"]);

@ -0,0 +1,7 @@
#notification_area
.notification-popup
.notification-header
.notification-contents
.error-popup
.notification-header
.notification-contents

@ -1,11 +1,23 @@
'use strict';
// NOTE: NO LONGER USED
const express = require("express");
const bodyParser = require("body-parser");
const path = require("path");
const webpackDevMiddleware = require("webpack-dev-middleware");
const webpackHotMiddleware = require("webpack-hot-middleware");
const webpack = require("webpack")(require("./webpack.config.js"));
let app = express();
app.use(webpackDevMiddleware(webpack, {
publicPath: "/"
}));
app.use(webpackHotMiddleware(webpack));
app.set("view engine", "pug");
app.set("views", path.join(__dirname, "views"));

@ -208,70 +208,70 @@ window
.title
{
@include shadow;
position: absolute;
z-index: 2;
left: 0px;
right: 0px;
top: 0px;
cursor: default;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
border-top-left-radius: 10px;
border-top-right-radius: 10px;
height: 16px;
color: white;
font-size: 14px;
font-weight: bold;
padding: 4px;
padding-left: 7px;
border-top: 1px solid #959595;
border-right: 1px solid #959595;
border-left: 1px solid #959595;
background-image: -webkit-gradient(
linear,
left bottom,
left top,
color-stop(0, rgb(82,82,82)),
color-stop(1, rgb(145,172,190))
);
background-image: -moz-linear-gradient(
center bottom,
rgb(82,82,82) 0%,
rgb(145,172,190) 100%
);
filter:alpha(opacity=95);
opacity:0.95;
// @include shadow;
// position: absolute;
// z-index: 2;
// left: 0px;
// right: 0px;
// top: 0px;
// cursor: default;
// text-overflow: ellipsis;
// overflow: hidden;
// white-space: nowrap;
// border-top-left-radius: 10px;
// border-top-right-radius: 10px;
// height: 16px;
// color: white;
// font-size: 14px;
// font-weight: bold;
// padding: 4px;
// padding-left: 7px;
// border-top: 1px solid #959595;
// border-right: 1px solid #959595;
// border-left: 1px solid #959595;
// background-image: -webkit-gradient(
// linear,
// left bottom,
// left top,
// color-stop(0, rgb(82,82,82)),
// color-stop(1, rgb(145,172,190))
// );
// background-image: -moz-linear-gradient(
// center bottom,
// rgb(82,82,82) 0%,
// rgb(145,172,190) 100%
// );
// filter:alpha(opacity=95);
// opacity:0.95;
}
.outer
{
@include shadow;
position: absolute;
z-index: 3;
left: 0px;
right: 0px;
bottom: 0px;
font-size: 13px;
top: 25px;
border-bottom: 1px solid gray;
border-right: 1px solid gray;
border-left: 1px solid gray;
background-color: #F7F7F0;
filter:alpha(opacity=95);
opacity:0.95;
// @include shadow;
// position: absolute;
// z-index: 3;
// left: 0px;
// right: 0px;
// bottom: 0px;
// font-size: 13px;
// top: 25px;
// border-bottom: 1px solid gray;
// border-right: 1px solid gray;
// border-left: 1px solid gray;
// background-color: #F7F7F0;
// filter:alpha(opacity=95);
// opacity:0.95;
}
.inner-wrapper
{
position: absolute;
top: 0px;
bottom: 0px;
left: 0px;
right: 0px;
overflow-y: auto;
overflow-x: auto;
// position: absolute;
// top: 0px;
// bottom: 0px;
// left: 0px;
// right: 0px;
// overflow-y: auto;
// overflow-x: auto;
}
.inner

@ -0,0 +1,67 @@
'use strict';
const Promise = require("bluebird");
const gulp = require("gulp");
const gulpNamedLog = require("gulp-named-log");
const gulpNodemon = require("gulp-nodemon");
const presetSCSS = require("@joepie91/gulp-preset-scss");
const awaitServer = require("await-server");
const gulpLivereload = require("gulp-livereload");
const patchLivereloadLogger = require("@joepie91/gulp-partial-patch-livereload-logger");
patchLivereloadLogger(gulpLivereload);
let config = {
scss: {
source: "./scss/**/*.scss",
destination: "./public/"
}
};
let serverLogger = gulpNamedLog("server");
gulp.task("nodemon", ["scss", "livereload"], () => {
gulpNodemon({
script: "app.js",
ignore: [
"gulpfile.js",
"node_modules",
"public",
"src/frontend"
],
ext: "js pug"
}).on("start", () => {
Promise.try(() => {
serverLogger.info("Starting...");
return awaitServer(3000);
}).then(() => {
serverLogger.info("Started!");
gulpLivereload.changed("*");
});
});
});
gulp.task("scss", () => {
return gulp.src(["./scss/style.scss", "./scss/components.scss"])
.pipe(presetSCSS({
livereload: gulpLivereload,
cacheKey: false
}))
.pipe(gulp.dest(config.scss.destination));
});
gulp.task("livereload", () => {
gulpLivereload.listen({
quiet: true
});
});
gulp.task("watch-css", () => {
gulp.watch(config.scss.source, ["scss"]);
});
gulp.task("watch", ["nodemon", "watch-css"]);
gulp.task("default", ["watch"]);

@ -1,3 +1,5 @@
"use strict";
const Promise = require("bluebird");
const pathToRegexp = require("path-to-regexp");
const url = require("url");

@ -0,0 +1,43 @@
'use strict';
const webpack = require("webpack");
const path = require("path");
module.exports = {
watch: true,
mode: "development",
entry: {
main: [
"webpack-hot-middleware/client?overlay=true&reload=true",
"./frontend/index.jsx"
]
},
output: {
publicPath: "/",
path: path.join(__dirname, "public/static/"),
filename: "bundle.js"
},
module: {
rules: [{
test: /\.jsx?$/,
exclude: /node_modules|src\/testcases/,
resolve: {
extensions: [".js", ".jsx"]
},
use: [{
loader: require.resolve("babel-loader"),
query: {
presets: [
"es2015",
"react"
].map(item => require.resolve(`babel-preset-${item}`))
}
}]
}]
},
plugins: [
new webpack.HotModuleReplacementPlugin(),
new webpack.NoEmitOnErrorsPlugin()
],
devtool: "source-map"
};

@ -9,40 +9,54 @@
"arraydiff": "^0.1.3",
"bhttp": "^1.2.1",
"bluebird": "^3.0.6",
"body-parser": "^1.15.1",
"body-parser": "^1.18.3",
"chokidar": "^2.0.4",
"classnames": "^2.2.5",
"copy-props": "^2.0.4",
"create-error": "^0.3.1",
"create-event-emitter": "^1.0.0",
"create-react-class": "^15.6.3",
"debounce": "^1.1.0",
"debug": "^2.2.0",
"default-value": "^1.0.0",
"elasticsearch": "^13.0.0-rc2",
"express": "^4.13.3",
"document-ready-promise": "^3.0.1",
"euclidean-distance": "^1.0.0",
"express": "^4.16.3",
"express-promise-router": "^1.0.0",
"form-serialize": "^0.7.2",
"immutable": "^3.8.2",
"in-array": "^0.1.2",
"jquery": "^3.2.1",
"mkdirp": "^0.5.1",
"moment": "^2.22.2",
"nanoid": "^2.0.0",
"object-to-formdata": "^1.0.9",
"pug": "^2.0.0-beta11",
"qs": "^6.4.0",
"rfr": "^1.2.3",
"riot": "^3.4.2",
"riot-query": "^1.0.0",
"uuid": "^3.0.1",
"react": "^16.7.0-alpha.0",
"react-dom": "^16.7.0-alpha.0",
"sse-channel": "^3.1.1",
"throttleit": "^1.0.0",
"unhandled-rejection": "^1.0.0",
"use-force-update": "^1.0.0",
"uuid": "^3.3.2",
"xtend": "^4.0.1"
},
"devDependencies": {
"@joepie91/gulp-preset-es2015": "^1.0.1",
"@joepie91/gulp-preset-scss": "^1.0.3",
"babel-loader": "^6.2.4",
"babel-preset-es2015": "^6.6.0",
"babel-preset-es2015-riot": "^1.1.0",
"debounce": "^1.0.0",
"@babel/core": "^7.1.5",
"@babel/preset-env": "^7.1.5",
"@babel/preset-react": "^7.0.0",
"await-server": "^1.0.0",
"babelify": "^10.0.0",
"browserify-hmr": "^0.3.6",
"budo": "^11.5.0",
"chalk": "^2.4.1",
"element-size": "^1.1.1",
"gulp": "^3.9.0",
"gulp-livereload": "^3.8.1",
"gulp-nodemon": "^2.0.4",
"gulp-rename": "^1.2.0",
"eslint": "^4.19.1",
"eslint-plugin-react": "^7.7.0",
"eslint-plugin-react-hooks": "^0.0.0",
"json-loader": "^0.5.4",
"node-sass": "^4.5.2",
"node-sass": "^4.10.0",
"path-to-regexp": "^1.2.1",
"riotjs-loader": "^4.0.0",
"webpack-stream-fixed": "^3.2.2"
"react-hot-loader": "4.4.0-1"
}
}

@ -0,0 +1,76 @@
.windowManager {
position: absolute;
left: 0px;
right: 0px;
top: 0px;
bottom: 0px; }
.window {
position: absolute;
left: 0px;
top: 0px;
display: grid;
grid-template-rows: auto 1fr;
box-shadow: 5px 5px 10px #1a1a1a;
opacity: 0.95; }
.window, .window .titleBar {
border-top-left-radius: 10px;
border-top-right-radius: 10px; }
.window .titleBar {
display: grid;
grid-template-columns: 1fr auto;
user-select: none;
-ms-user-select: none;
-moz-user-select: none;
-webkit-user-select: none;
cursor: default;
border: 1px solid #959595;
border-bottom: none;
padding: 4px;
padding-left: 7px;
background: linear-gradient(to bottom, #525252 0%, #91acbe 100%); }
.window .titleBar .title {
padding-top: 1px;
color: white;
font-size: 14px;
font-weight: bold;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap; }
.window .titleBar .buttons {
display: flex;
margin-left: 8px; }
.window .titleBar .buttons .button {
padding: 2px;
border: 1px solid #014D8C;
border-radius: 5px;
color: white;
font-weight: bold; }
.window .titleBar .buttons .button:hover {
background-color: #014D8C;
border: 1px solid white; }
.window .titleBar .buttons .button.close {
font-size: 12px;
line-height: 1; }
.window .body {
position: relative;
background-color: #F7F7F0;
border: 1px solid gray;
border-top: none; }
.window .body .content {
overflow-x: auto;
overflow-y: auto;
padding: 7px;
/* FIXME: Is this needed here? */
font-size: 13px; }
.window .body .resizer {
position: absolute;
/* FIXME: Make the below values configurable? */
bottom: -6px;
right: -6px;
width: 12px;
height: 12px;
cursor: se-resize; }
.window.active .titleBar {
background: linear-gradient(to bottom, #0057b3 0%, #0099ff 100%);
border-color: #6262FF; }

@ -0,0 +1,142 @@
.viewManager {
width: 100%; }
.windowManager {
position: absolute;
left: 0px;
right: 0px;
top: 0px;
bottom: 0px; }
.window {
position: absolute;
left: 0px;
top: 0px;
display: grid;
grid-template-rows: auto 1fr;
box-shadow: 5px 5px 10px #1a1a1a;
opacity: 0.95; }
.window, .window .titleBar {
border-top-left-radius: 10px;
border-top-right-radius: 10px; }
.window .titleBar {
display: grid;
grid-template-columns: 1fr auto;
user-select: none;
-ms-user-select: none;
-moz-user-select: none;
-webkit-user-select: none;
cursor: default;
border: 1px solid #959595;
border-bottom: none;
padding: 4px;
padding-left: 7px;
background: linear-gradient(to bottom, #525252 0%, #91acbe 100%); }
.window .titleBar .title {
padding-top: 1px;
color: white;
font-size: 14px;
font-weight: bold;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap; }
.window .titleBar .buttons {
display: flex;
margin-left: 8px; }
.window .titleBar .buttons .button {
padding: 2px;
border: 1px solid #526371;
border-radius: 5px;
color: white;
font-weight: bold; }
.window .titleBar .buttons .button:hover {
background-color: #555f68;
border: 1px solid #ada8a8; }
.window .titleBar .buttons .button.close {
font-size: 12px;
line-height: 1; }
.window .body {
position: relative;
background-color: #F7F7F0;
border: 1px solid gray;
border-top: none; }
.window .body .content {
overflow-x: auto;
overflow-y: auto;
padding: 7px;
/* FIXME: Is this needed here? */
font-size: 13px; }
.window .body .resizer {
user-select: none;
-ms-user-select: none;
-moz-user-select: none;
-webkit-user-select: none;
position: absolute;
/* FIXME: Make the below values configurable? */
bottom: -8px;
right: -8px;
width: 16px;
height: 16px;
cursor: se-resize; }
.window .body .resizer:hover .arrow {
position: relative;
left: -4px;
top: -4px;
width: 0px;
height: 0px;
border-style: solid;
border-width: 0px 0px 12px 12px;
border-color: transparent transparent rgba(0, 109, 201, 0.8) transparent; }
.window.active .titleBar {
background: linear-gradient(to bottom, #0057b3 0%, #0099ff 100%);
border-color: #6262FF; }
.window.active .titleBar .buttons .button {
border: 1px solid #014D8C; }
.window.active .titleBar .buttons .button:hover {
background-color: #0e5c9c;
border: 1px solid #7ab0e6; }
#notificationArea {
position: absolute;
right: 0px;
bottom: 32px;
z-index: 2147483640;
display: flex;
flex-direction: column; }
.notification {
display: inline-block;
border-radius: 6px;
margin-right: 19px;
margin-top: 10px;
padding: 9px 14px;
color: white;
font-size: 15px;
filter: alpha(opacity=85);
opacity: 0.85;
width: auto;
min-width: 220px; }
.notification .closeButton {
float: right;
margin-top: -4px;
margin-right: -6px;
margin-left: 6px;
margin-bottom: 4px;
padding: 4px 6px;
opacity: 0.6;
border-radius: 3px;
cursor: default; }
.notification .closeButton:hover {
opacity: 1;
background-color: rgba(30, 30, 30, 0.6); }
.notification .header {
margin-right: 6px;
font-weight: bold;
margin-bottom: 6px; }
.notification .contents ul {
margin: 4px 0px;
padding-left: 48px; }
.notification.type-info {
background-color: #2D2D2D; }
.notification.type-error {
background-color: #371B1B; }

@ -0,0 +1,616 @@
body {
background-image: url(/images/background.jpg); }
#logo {
z-index: 0;
display: inline;
position: relative;
top: -24px;
margin-left: 20px;
color: white;
font-size: 64px;
font-family: 'Istok Web';
font-weight: bold;
color: #E2FFFF;
text-shadow: 0px 0px 1px #CEE3F9;
-webkit-text-shadow: 0px 0px 1px #CEE3F9;
-moz-text-shadow: 0px 0px 1px #CEE3F9;
-o-text-shadow: 0px 0px 1px #CEE3F9;
-ms-text-shadow: 0px 0px 1px #CEE3F9; }
.clear {
clear: both; }
form.pure-form-inline {
display: inline; }
.autocompleter {
display: none;
position: absolute;
/* TODO: set this in the JS! */
z-index: 9999999;
-webkit-font-smoothing: antialiased;
font-family: 'Istok Web';
background-color: white;
border: 1px solid #5A6DBB;
border-radius: 0px 0px 3px 3px;
border-top: none; }
.autocompleter .entry {
color: #393939; }
.autocompleter .entry.selected {
background-color: #4253B6;
color: white; }
#autocomplete_search {
width: 400px; }
#autocomplete_search .entry {
border-bottom: 1px solid #C2C2C2;
padding: 7px 9px; }
#autocomplete_search .entry:last-child {
border-bottom: none; }
#autocomplete_search .entry .name {
font-size: 18px;
font-weight: bold; }
#autocomplete_search .entry .description, #autocomplete_search .entry .date {
font-size: 14px; }
#autocomplete_search .entry .date {
float: right;
color: gray; }
#autocomplete_search .entry.selected .date {
color: white; }
#autocomplete_search .loading, #autocomplete_search .noresults {
padding: 8px 10px;
font-size: 17px; }
#autocomplete_search .loading {
font-style: italic; }
.group-first, .group-middle {
border-top-right-radius: 0px !important;
border-bottom-right-radius: 0px !important; }
.group-middle, .group-last {
border-top-left-radius: 0px !important;
border-bottom-left-radius: 0px !important; }
#mainToolbar {
display: flex;
padding: 36px 24px; }
#mainToolbar, #mainToolbar button {
font-family: "Istok Web"; }
#mainToolbar a.add, #mainToolbar input, #mainToolbar button {
display: block;
box-sizing: border-box;
height: 40px; }
#mainToolbar a.add {
float: left; }
#mainToolbar form.search, #mainToolbar form.download {
display: inline-flex;
margin-left: 12px; }
#mainToolbar form.search input, #mainToolbar form.download input {
margin-left: 12px;
font-size: 16px;
padding: 7px 14px;
border-top-right-radius: 0px;
border-bottom-right-radius: 0px;
width: 290px; }
#mainToolbar form.search button, #mainToolbar form.download button {
border-top-left-radius: 0px;
border-bottom-left-radius: 0px; }
#mainToolbar form.download {
margin-left: auto; }
.pure-button {
padding: 9px 14px 7px 14px; }
.pure-button.shadow {
text-shadow: 1px 1px 1px #424242;
-webkit-text-shadow: 1px 1px 1px #424242;
-moz-text-shadow: 1px 1px 2px #424242;
-o-text-shadow: 1px 1px 1px #424242;
-ms-text-shadow: 1px 1px 1px #424242; }
.pure-button.add {
background-color: #15BA31;
color: white; }
.pure-button.okay {
background-color: #148C29;
color: white; }
.pure-button.search {
background-color: #152DBA;
color: white; }
.pure-button.download {
background-color: #1591ba;
color: white; }
.pure-button i {
margin-right: 8px;
position: relative;
top: 1px; }
.element-group {
display: block;
float: left; }
.element-group * {
float: left; }
.formSection {
margin-bottom: 5px; }
.formSection h1.formSectionTitle {
font-size: 16px;
margin-top: 0px;
margin-bottom: 6px; }
.formField .labelWrapper {
display: inline-block;
*display: inline;
zoom: 1;
letter-spacing: normal;
word-spacing: normal;
vertical-align: top;
text-rendering: auto;
width: 33.3333%;
*width: 33.3023%;
padding-top: 4px; }
.formField .labelWrapper label {
font-size: 95%;
margin-left: 1px; }
.formField:not(.grouped):not(.unlabeled) .inputWrapper {
display: inline-block;
*display: inline;
zoom: 1;
letter-spacing: normal;
word-spacing: normal;
vertical-align: top;
text-rendering: auto;
width: 66.6667%;
*width: 66.6357%; }
.formField input, .formField textarea {
width: 100%; }
.formField input.halfWidth, .formField textarea.halfWidth {
display: inline-block;
*display: inline;
zoom: 1;
letter-spacing: normal;
word-spacing: normal;
vertical-align: top;
text-rendering: auto;
width: 50%;
*width: 49.9690%; }
.formField input.invalid, .formField textarea.invalid {
border-color: #B60202 !important; }
.formField textarea {
height: 90px; }
.formField.unlabeled:not(.grouped) .inputWrapper {
display: inline-block;
*display: inline;
zoom: 1;
letter-spacing: normal;
word-spacing: normal;
vertical-align: top;
text-rendering: auto;
width: 100%; }
.formField.grouped .inputWrapper {
float: left; }
.formField.grouped .inputWrapper input, .formField.grouped .inputWrapper textarea, .formField.grouped .inputWrapper button {
border-radius: 0px;
border-right-width: 0px; }
.formField.grouped .inputWrapper:first-child input, .formField.grouped .inputWrapper:first-child textarea, .formField.grouped .inputWrapper:first-child button {
border-top-left-radius: 4px;
border-bottom-left-radius: 4px; }
.formField.grouped .inputWrapper:last-child input, .formField.grouped .inputWrapper:last-child textarea, .formField.grouped .inputWrapper:last-child button {
border-top-right-radius: 4px;
border-bottom-right-radius: 4px; }
.formField.grouped .inputWrapper:last-child input, .formField.grouped .inputWrapper:last-child textarea {
border-right-width: 1px; }
.toolbarWindow.hasTop .toolbarWindowContents {
top: 38px; }
.toolbarWindow.hasBottom .toolbarWindowContents {
bottom: 38px; }
.toolbarWindow.hasLeft .toolbarWindowContents {
left: 38px; }
.toolbarWindow.hasRight .toolbarWindowContents {
right: 38px; }
.toolbarWindow .toolbarWindowContents {
position: absolute;
padding: 7px;
top: 0px;
bottom: 0px;
left: 0px;
right: 0px;
overflow-x: hidden;
overflow-y: auto; }
.toolbarWindow .toolbarWindowControls {
position: absolute;
padding: 4px;
text-align: right; }
.toolbarWindow .toolbarWindowControls.top, .toolbarWindow .toolbarWindowControls.bottom {
height: 30px;
left: 0px;
right: 0px; }
.toolbarWindow .toolbarWindowControls.top {
top: 0px; }
.toolbarWindow .toolbarWindowControls.bottom {
bottom: 0px; }
.toolbarWindow .toolbarWindowControls.left, .toolbarWindow .toolbarWindowControls.right {
width: 30px;
top: 0px;
bottom: 0px; }
.toolbarWindow .toolbarWindowControls.left {
left: 0px; }
.toolbarWindow .toolbarWindowControls.right {
right: 0px; }
.pure-button.style-okay {
background-color: #148C29;
color: white; }
div.formfield, div.property {
margin-bottom: 1px; }
.toolbarwindow-contents {
position: absolute;
padding: 7px;
top: 0px;
left: 0px;
right: 0px;
bottom: 38px;
overflow-x: hidden;
overflow-y: auto; }
.toolbarwindow-toolbar {
position: absolute;
padding: 4px;
height: 30px;
left: 0px;
right: 0px;
bottom: 0px;
text-align: right; }
i.required {
font-size: 10px;
margin-left: 4px;
color: #9f444a;
float: right;
margin-right: 7px;
margin-top: 2px; }
i.error, i.notification {
font-size: 19px;
margin-right: 9px !important; }
i.error {
color: #FFD2D2; }
i.notification {
color: #CBCAFF; }
#notification_area {
position: absolute;
right: 0px;
bottom: 32px;
z-index: 2147483640; }
.notification-header {
margin-right: 6px;
font-weight: bold; }
.notification-popup, .error-popup {
text-align: right; }
.notification-popup .notification-contents, .error-popup .notification-contents {
text-align: left;
display: inline-block;
border-radius: 6px;
margin-right: 19px;
margin-top: 10px;
padding: 9px 14px;
color: white;
font-size: 15px;
filter: alpha(opacity=85);
opacity: 0.85;
width: auto; }
.notification-popup .notification-contents {
background-color: #2D2D2D; }
.error-popup .notification-contents {
background-color: #371B1B; }
.notification-popup ul, .error-popup ul {
margin: 4px 0px;
padding-left: 48px; }
#autocomplete_propertyname {
font-size: 13px; }
#autocomplete_propertyname .entry, #autocomplete_propertyname .noresults, #autocomplete_propertyname .loading {
padding: 4px 6px; }
.window-inner .header, .window-inner h1, .window-inner h2 {
margin: 4px 0px; }
.window-inner .lower-header, .window-inner h2 {
margin-top: 7px; }
.window-inner h1 {
font-size: 20px; }
.window-inner h2 {
font-size: 16px; }
.window-inner .unit-content {
padding: 6px; }
.window-inner a.source-ref {
float: right;
margin-right: -4px;
text-decoration: none;
color: gray;
font-size: 13px; }
.window-inner a.source-ref:hover {
color: black; }
.window-inner table {
font-size: 13px;
width: 100%; }
.window-inner td.property-name {
width: 40%;
text-align: right;
font-weight: bold; }
.window-inner .node-lookup form.property-add input {
width: 100%; }
.window-inner .node-lookup form.property-add .property-name input {
text-align: right; }
html, body {
overflow: hidden; }
body {
position: fixed;
margin: 0px;
width: 100%;
height: 100%; }
#jsde_templates {
display: none; }
/* MDIWindow Styling | Generic */
div.window-wrapper {
position: absolute; }
div.window-title {
position: absolute;
z-index: 2;
left: 0px;
right: 0px;
top: 0px;
cursor: default;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap; }
div.window-outer {
position: absolute;
z-index: 3;
left: 0px;
right: 0px;
bottom: 0px; }
div.window-inner-wrapper {
position: absolute;
top: 0px;
bottom: 0px;
left: 0px;
right: 0px;
overflow-y: auto;
overflow-x: auto; }
.window-noscroll {
overflow-x: hidden !important;
overflow-y: hidden !important; }
div.window-close {
float: right; }
div.window-close a {
position: absolute;
right: 3px;
top: 2px;
display: block;
padding: 1px 4px;
text-decoration: none;
font-size: 12px;
border-radius: 5px; }
/* MDIWindow Styling | Normal state */
div.window-styled div.window-inner {
visibility: visible; }
/* MDIWindow Styling | Dragging state */
div.window-dragged div.window-inner {
visibility: hidden; }
div.workspace-bar {
position: absolute;
bottom: 0px;
left: 0px;
right: 0px; }
a.workspace-tab {
display: block;
float: left; }
div.window-resizer {
position: absolute;
width: 12px;
height: 12px;
bottom: -6px;
right: -6px;
cursor: se-resize; }
html, body {
font-family: 'Varela Round', sans-serif, Trebuchet MS;
background-color: silver;
background-attachment: fixed;
background-position: center; }
/* Temporary styles */
div#make-window {
background-color: white;
border: 1px solid gray;
padding: 14px;
width: 350px;
filter: alpha(opacity=85);
opacity: 0.85; }
/* MDIWindow Styling | Generic */
div.window-title {
-webkit-border-top-left-radius: 10px;
-webkit-border-top-right-radius: 10px;
-moz-border-radius-topleft: 10px;
-moz-border-radius-topright: 10px;
border-top-left-radius: 10px;
border-top-right-radius: 10px;
height: 16px;
color: white;
font-size: 14px;
font-weight: bold;
padding: 4px;
padding-left: 7px;
border-top: 1px solid #959595;
border-right: 1px solid #959595;
border-left: 1px solid #959595; }
div.window-focused div.window-title {
border-top: 1px solid #6262FF;
border-right: 1px solid #6262FF;
border-left: 1px solid #6262FF; }
div.window-outer {
font-size: 13px;
top: 25px;
border-bottom: 1px solid gray;
border-right: 1px solid gray;
border-left: 1px solid gray; }
div.window-inner {
padding: 7px; }
div.window-close a {
color: white;
border: 1px solid #014D8C; }
div.window-close a:hover {
background-color: #014D8C;
border: 1px solid white; }
/* MDIWindow Styling | Normal state */
div.window-styled div.window-title, div.window-styled div.window-outer {
-webkit-box-shadow: 5px 5px 10px #1a1a1a;
-moz-box-shadow: 5px 5px 10px #1a1a1a;
box-shadow: 5px 5px 10px #1a1a1a; }
div.window-styled div.window-title {
background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0, #525252), color-stop(1, #91acbe));
background-image: -moz-linear-gradient(center bottom, #525252 0%, #91acbe 100%);
filter: alpha(opacity=95);
opacity: 0.95; }
div.window-styled div.window-outer {
background-color: #F7F7F0;
filter: alpha(opacity=95);
opacity: 0.95; }
div.window-focused.window-styled div.window-title {
background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0, #0057b3), color-stop(1, #0099ff));
background-image: -moz-linear-gradient(center bottom, #0057b3 0%, #0099ff 100%);
filter: alpha(opacity=85);
opacity: 0.85; }
/* MDIWindow Styling | Dragging state */
div.window-dragged div.window-title {
background-color: #0070D5;
background-image: none; }
div.workspace-bar {
height: 32px; }
div.window-dragged div.window-outer {
background: none; }
div.window-dragged div.window-title, div.window-dragged div.window-outer {
-webkit-box-shadow: none;
-moz-box-shadow: none;
box-shadow: none; }
a.workspace-tab {
height: 32px;
width: 48px;
-webkit-border-top-left-radius: 10px;
-webkit-border-top-right-radius: 10px;
-moz-border-radius-topleft: 10px;
-moz-border-radius-topright: 10px;
border-top-left-radius: 10px;
border-top-right-radius: 10px;
border: 1px solid black;
background-color: #E9E9E9;
padding-top: 6px;
text-align: center;
text-decoration: none;
font-size: 14px;
color: black;
margin-top: 10px;
filter: alpha(opacity=95);
opacity: 0.95; }
a.workspace-tab:hover {
background-color: #DADADA;
margin-top: 0px; }
a.workspace-tab-active {
font-weight: bold;
background-color: #B9B9B9;
color: #BC0000; }
a.workspace-tab-popup {
margin-top: 0px; }
a.workspace-tab-add {
background-color: #C8C8C8;
filter: alpha(opacity=85);
opacity: 0.85; }
window {
/* In order for stacked z-index to work, and prevent the segments of a window from
* overlapping due to z-index conflicts, we *have* to specify positioning for the
* `window` wrapper element. This way a stacking context is created, and the z-index
* conflicts go away.
*/
position: absolute;
left: 0px;
top: 0px; }

File diff suppressed because it is too large Load Diff

@ -1,44 +0,0 @@
const Promise = require("bluebird");
const rfr = require("rfr");
const errors = rfr("lib/util/errors");
let router = require("express-promise-router")();
router.get("/", (req, res) => {
res.render("layout");
});
router.get("/node/:uuid", (req, res) => {
return Promise.try(() => {
return db.Node.find(req.params.uuid);
}).then((node) => {
res.json(node);
}).catch(db.Node.NotFoundError, (err) => {
throw new errors.NotFoundError("Could not find a Node with that UUID");
});
});
router.post("/autocomplete/type", (req, res) => {
return Promise.try(() => {
// FIXME: suggest, https://www.elastic.co/guide/en/elasticsearch/reference/current/search-suggesters.html
return elasticCloud.search({
index: "types"
})
})
})
router.post("/nodes/create", (req, res) => {
res.json({
result: "success",
body: req.body
});
});
router.get("/test1", (req, res) => {
res.send("test ONE <a href='/test2'>go to 2 instead</a> <a href='/test2' target='_blank'>or in a new window</a>");
});
router.get("/test2", (req, res) => {
res.send("test TWO <a href='/test1'>go to 1 instead</a> <a href='/test1' target='_blank'>or in a new window</a>");
});
module.exports = router;

@ -0,0 +1,15 @@
"use strict";
const React = require("react");
const Notification = require("./notification");
module.exports = function NotificationArea({store}) {
return (
<div id="notificationArea">
{store.getVisible().toArray().map((notificationData) => {
return <Notification key={notificationData.id} store={store} {...notificationData} />;
})}
</div>
);
};

@ -0,0 +1,27 @@
"use strict";
const React = require("react");
const classnames = require("classnames");
function CloseButton({onClick}) {
return (
<div className="closeButton" onClick={onClick}></div>
);
}
module.exports = function Notification({store, id, type, title, message, controls}) {
return (
<div className={classnames("notification", `type-${type}`)}>
<CloseButton onClick={() => store.markRead(id)} />
<div className="header">
{title}
</div>
<div className="contents">
{message}
</div>
<div className="controls">
{controls}
</div>
</div>
);
};

@ -0,0 +1,5 @@
"use strict";
const React = require("react");
module.exports = React.createContext();

@ -0,0 +1,38 @@
"use strict";
const React = require("react");
const cold = require("react-hot-loader").cold;
const formSerialize = require("form-serialize");
const context = require("./context");
module.exports = cold(function Form(props) {
let { handle } = React.useContext(context);
let {
method, url, children, multipart,
/* We ignore the following two, as they are covered by `multipart` and `url` respectively. */
enctype: _enctype,
action: _action,
...rest
} = props;
function submitFormToRouter(form) {
let serializedFormData = formSerialize(form, {hash: true, empty: true, disabled: true});
return handle(method, url, serializedFormData, {
multipart: (multipart === true)
});
}
function handleSubmit(event) {
event.preventDefault();
return submitFormToRouter(event.target);
}
return (
<form onSubmit={handleSubmit} encType={(multipart === true ? "multipart/form-data" : null)} {...rest}>
{children}
</form>
);
});

@ -0,0 +1,7 @@
"use strict";
module.exports = {
ViewManager: require("./view-manager"),
Link: require("./link"),
Form: require("./form")
};

@ -0,0 +1,24 @@
"use strict";
const React = require("react");
const context = require("./context");
function Link(props) {
let { handle } = React.useContext(context);
let {
url, children,
/* The following is covered by the `url` prop already. */
href: _href,
...rest
} = props;
return (
<a href="#" onClick={() => handle("get", url)} {...rest}>
{children}
</a>
);
}
module.exports = Link;

@ -0,0 +1,92 @@
"use strict";
const Promise = require("bluebird");
const React = require("react");
const propTypes = require("prop-types");
const defaultValue = require("default-value");
const useForceUpdate = require("use-force-update").default;
const context = require("./context");
function NoView({isLoading, loadingSince, spinnerDelay}) {
if (isLoading) {
if (Date.now() - loadingSince > spinnerDelay) {
/* FIXME: Show a better loading indicator. */
return "Loading...";
} else {
return null;
}
} else {
return "ERROR: View missing.";
}
}
function ViewManager({router, initialPath, onAction, spinnerDelay}) {
let [isLoading, setIsLoading] = React.useState(false);
let [loadingSince, setLoadingSince] = React.useState(null);
let [View, setView] = React.useState(null);
let [locals, setLocals] = React.useState({});
/* FIXME: Shouldn't the viewOptions be used? */
let [_viewOptions, setViewOptions] = React.useState({});
let forceUpdate = useForceUpdate();
let spinnerDelay_ = defaultValue(spinnerDelay, 100);
function handle(method, path, data) {
return Promise.try(() => {
setIsLoading(true);
setLoadingSince(Date.now());
/* FIXME: This is a bit hacky. Isn't there a better way to handle matters of timing in React? */
setTimeout(() => {
forceUpdate();
}, spinnerDelay_);
return router.handle(method, path, data);
}).then((result) => {
result.actions.forEach((action) => {
// console.log("ACTION", action);
if (action.type === "render") {
setView({Component: action.view});
setLocals(action.locals);
setViewOptions(action.options);
} else if (action.type === "redirect") {
return handle(defaultValue(action.method, method), action.path);
} else {
if (onAction != null) {
onAction(action);
} else {
console.error(`Action of type '${action.type}' got lost due to missing onAction handle`);
}
}
});
});
}
React.useEffect(() => {
if (initialPath != null) {
/* FIXME: Display loading indicator? */
handle("get", initialPath);
}
}, []);
/* MARKER/FIXME: Initial URL, and how to produce the children of a ViewManager based on the most recent router navigation; responses need to persist even on re-renders, without re-creating backend requests... Also, how to deal with 'forcible' navigation from upstream in the DOM tree, when the initialUrl is modified *after* the ViewManager was instantiated? */
return (
<context.Provider value={{handle}}>
{(View != null)
? <View.Component {...locals} />
: <NoView isLoading={isLoading} loadingSince={loadingSince} spinnerDelay={spinnerDelay_} />}
</context.Provider>
);
}
ViewManager.propTypes = {
router: propTypes.any.isRequired,
initialPath: propTypes.string.isRequired,
onAction: propTypes.func
};
module.exports = ViewManager;

@ -0,0 +1,180 @@
'use strict';
const React = require("react");
const createReactClass = require("create-react-class");
const throttleit = require("throttleit");
const euclideanDistance = require("euclidean-distance");
const defaultValue = require("default-value");
const Window = require("./window");
/* These can be stored outside a component as effective globals, because there can only be one mouse position anyway. */
let lastMouseX = 0;
let lastMouseY = 0;
let windowRefs = new Map();
function trackRef(window_) {
return function setRef(element) {
/* FIXME: Verify that this doesn't leak memory! */
if (element != null) {
windowRefs.set(window_, element);
} else {
windowRefs.delete(window_);
}
};
}
module.exports = function WindowManager({store, children, windowMoveThreshold_}) {
let [movingType, setMovingType] = React.useState(null);
let [movingWindow, setMovingWindow] = React.useState(null);
let [movingWindowBarX, setMovingWindowBarX] = React.useState(null);
let [movingWindowBarY, setMovingWindowBarY] = React.useState(null);
let [movingWindowOriginalWidth, setMovingWindowOriginalWidth] = React.useState(null);
let [movingWindowOriginalHeight, setMovingWindowOriginalHeight] = React.useState(null);
let [movingWindowOriginX, setMovingWindowOriginX] = React.useState(null);
let [movingWindowOriginY, setMovingWindowOriginY] = React.useState(null);
let [movingWindowThresholdMet, setMovingWindowThresholdMet] = React.useState(false);
let [processMouseMove, setProcessMouseMove] = React.useState(null);
let windowMoveThreshold = defaultValue(windowMoveThreshold_, 6);
React.useEffect(() => {
processMouseMove = throttleit(() => {
/* Yes, the below check is there for a reason; just in case the `movingWindow` state changes between the call to the throttled wrapper and the wrapped function itself. */
if (movingWindow != null) {
let thresholdNewlyMet = false;
if (movingWindowThresholdMet === false) {
let origin = [movingWindowOriginX, movingWindowOriginY];
let position = [lastMouseX, lastMouseY];
if (euclideanDistance(origin, position) > windowMoveThreshold) {
thresholdNewlyMet = true;
setMovingWindowThresholdMet(true);
}
}
if (movingWindowThresholdMet || thresholdNewlyMet === true) {
/* Note: we handle this out-of-band, to avoid going through React's rendering cycle for every move event. */
let element = windowRefs.get(movingWindow);
if (movingType === "move") {
let {x, y} = getCurrentMoveValues();
element.style.transform = `translate(${x}px, ${y}px)`;
} else if (movingType === "resize") {
let {width, height} = getCurrentResizeValues();
element.style.width = `${width}px`;
element.style.height = `${height}px`;
}
}
}
}, 10);
/* NOTE: We first manually set the processMouseMove above, to ensure that the new function instance is immediately available in the render code below. */
/* HACK: React interprets functions as lazy setters, therefore we return the wrapped processMouseMove function from an arrow function. */
setProcessMouseMove(() => processMouseMove);
}, [movingWindow]);
function getCurrentMoveValues() {
return {
x: lastMouseX - movingWindowBarX,
y: lastMouseY - movingWindowBarY
};
}
function getCurrentResizeValues() {
return {
width: movingWindowOriginalWidth + (lastMouseX - movingWindowOriginX),
height: movingWindowOriginalHeight + (lastMouseY - movingWindowOriginY)
};
}
/* NOTE: Due to how synthetic events work in React, we need to separate out the 'get coordinates from event' and 'do something with the coordinates' step; if we throttle the coordinate extraction logic, we'll run into problems when synthetic events have already been cleared for reuse by the time we try to obtain the coordinates. Therefore, `processMouseMove` contains all the throttled logic, whereas the coordinate extraction happens on *every* mousemove event. */
/* FIXME: Consider sidestepping React entirely for the mousemove event handling. How much do synthetic events slow things down? */
function handleMouseMove(event) {
if (movingWindow != null) {
lastMouseX = event.pageX;
lastMouseY = event.pageY;
processMouseMove();
}
}
function handleDragStart(type, window_, event, barX, barY) {
setMovingType(type);
setMovingWindow(window_.id);
setMovingWindowThresholdMet(false);
setMovingWindowBarX(barX);
setMovingWindowBarY(barY);
setMovingWindowOriginX(event.pageX);
setMovingWindowOriginY(event.pageY);
/* NOTE: The below is to ensure that the window doesn't jump, if the user clicks the titlebar but never moves the mouse. No mousemove event is fired in that scenario, so if we don't set the last-known coordinates here, the window would jump based on whatever coordinate the mouse was last at during the *previous* window drag operation. */
lastMouseX = event.pageX;
lastMouseY = event.pageY;
console.log("started drag of type", type);
}
function handleTitleMouseDown (window_, event, barX, barY) {
return handleDragStart("move", window_, event, barX, barY);
}
function handleResizerMouseDown(window_, event, barX, barY) {
setMovingWindowOriginalWidth(window_.width);
setMovingWindowOriginalHeight(window_.height);
return handleDragStart("resize", window_, event, barX, barY);
}
function handleMouseUp () {
if (movingWindow != null) {
if (movingType === "move") {
let {x, y} = getCurrentMoveValues();
store.setPosition(movingWindow, x, y);
} else if (movingType === "resize") {
let {width, height} = getCurrentResizeValues();
store.setDimensions(movingWindow, width, height);
}
setMovingWindow(null);
}
}
/* TODO: Tiled layout */
return (
<div className="windowManager" onMouseMove={handleMouseMove} onMouseUp={handleMouseUp}>
{children}
{store.getAll().toArray().map((window_) => {
let windowStyle = {
transform: `translate(${window_.x}px, ${window_.y}px)`,
width: window_.width,
height: window_.height,
zIndex: window_.zIndex
};
let handlers = {
onTitleMouseDown: (...args) => {
return handleTitleMouseDown(window_, ...args);
},
onResizerMouseDown: (...args) => {
return handleResizerMouseDown(window_, ...args);
},
onMouseDown: () => {
store.focus(window_.id);
},
onClose: () => {
store.close(window_.id);
}
};
return (
<Window elementRef={trackRef(window_.id)} key={window_.id} style={windowStyle} title={window_.title} isActive={window_.isActive} resizable={window_.resizable} {...handlers}>
{window_.contents}
</Window>
);
})}
</div>
);
};

@ -0,0 +1,128 @@
'use strict';
const React = require("react");
const createReactClass = require("create-react-class");
const throttleit = require("throttleit");
const euclideanDistance = require("euclidean-distance");
const Window = require("./window");
module.exports = createReactClass({
displayName: "WindowManager",
getDefaultProps: function () {
return {
windowMoveThreshold: 6
};
},
getInitialState: function () {
return {
movingWindow: null,
activeWindow: null,
windowPositions: {}
};
},
lastMousePositionX: null,
lastMousePositionY: null,
componentDidMount: function () {
/* Due to how synthetic events work in React, we need to separate out the 'get coordinates from event' and 'do something with the coordinates' step; if we throttle the coordinate extraction logic, we'll run into problems when synthetic events have already been cleared for reuse by the time we try to obtain the coordinates. Therefore, `processMouseMove` contains all the throttled logic, whereas the coordinate extraction happens on *every* mousemove event. */
this.processMouseMove = throttleit(() => {
/* Yes, the below check is there for a reason; just in case the `movingWindow` state changes between the call to the throttled wrapper and the wrapped function itself. */
if (this.state.movingWindow != null) {
let thresholdMet = this.state.movingWindowThresholdMet;
/* Since we're outside of a React-controlled handler, we can't rely on React to batch updates. Therefore, we have to do our own impromptu batching. */
let stateToUpdate = {};
if (thresholdMet === false) {
let origin = [this.state.movingWindowOriginX, this.state.movingWindowOriginY];
let position = [this.lastMousePositionX, this.lastMousePositionY];
if (euclideanDistance(origin, position) > this.props.windowMoveThreshold) {
thresholdMet = true;
stateToUpdate.thresholdMet = true;
}
}
if (thresholdMet === true) {
/* TODO: Consider handling this move out-of-band, to avoid going through React's rendering cycle for every move event. */
stateToUpdate.windowPositions = Object.assign(this.state.windowPositions, {
[this.state.movingWindow]: {
x: this.lastMousePositionX - this.state.movingWindowBarX,
y: this.lastMousePositionY - this.state.movingWindowBarY
}
});
}
/* FIXME: Move this into the store? */
this.setState(stateToUpdate);
}
}, 10);
},
handleMouseMove: function(event) {
if (this.state.movingWindow != null) {
this.lastMousePositionX = event.pageX;
this.lastMousePositionY = event.pageY;
this.processMouseMove();
}
},
handleMouseUp: function () {
this.setState({
movingWindow: null
});
},
handleTitleMouseDown: function (window, event, barX, barY) {
this.setState({
movingWindow: window.id,
movingWindowThresholdMet: false,
movingWindowBarX: barX,
movingWindowBarY: barY,
movingWindowOriginX: event.pageX,
movingWindowOriginY: event.pageY
});
},
render: function () {
/* TODO: Tiled layout */
return (
<div className="windowManager" onMouseMove={this.handleMouseMove} onMouseUp={this.handleMouseUp}>
{this.props.children}
{this.props.store.getAll().toArray().map((window_) => {
let x, y;
let overrideWindowPosition = this.state.windowPositions[window_.id];
if (overrideWindowPosition != null) {
x = overrideWindowPosition.x;
y = overrideWindowPosition.y;
} else {
x = window_.initialX;
y = window_.initialY;
}
let windowStyle = {
transform: `translate(${x}px, ${y}px)`,
width: window_.width,
height: window_.height,
zIndex: window_.zIndex
};
let handlers = {
onTitleMouseDown: (...args) => {
return this.handleTitleMouseDown(window_, ...args);
},
onMouseDown: () => {
this.props.store.focus(window_.id);
},
onClose: () => {
this.props.store.close(window_.id);
}
};
return (<Window key={window_.id} style={windowStyle} title={window_.title} isActive={window_.isActive} {...handlers}>
{window_.contents}
</Window>);
})}
</div>
);
}
});

@ -0,0 +1,94 @@
'use strict';
const React = require("react");
const createReactClass = require("create-react-class");
const classnames = require("classnames");
const renderIf = require("../util/render-if");
function cancelEvent(event) {
/* NOTE: This is used on mousedown events for controls like the CloseButton, to ensure that a click-and-drag on a titlebar control cannot start a window drag operation. */
event.stopPropagation();
}
function relativePosition(event, bounds) {
let relativeX = event.clientX - bounds.x;
let relativeY = event.clientY - bounds.y;
return {
x: relativeX,
y: relativeY
};
}
function CloseButton({onClick}) {
return (
<div className="button close" onClick={onClick} onMouseDown={cancelEvent}>
</div>
);
}
let TitleBar = createReactClass({
displayName: "TitleBar",
setRef: function (ref) {
this.setState({
ref: ref
});
},
handleMouseDown: function (event) {
let bounds = this.state.ref.getBoundingClientRect();
let relativeX = event.clientX - bounds.x;
let relativeY = event.clientY - bounds.y;
this.props.onMouseDown(event, relativeX, relativeY);
},
render: function () {
return (
<div ref={this.setRef} className="titleBar" onMouseDown={this.handleMouseDown}>
<div className="title">
{this.props.title}
</div>
<div className="buttons">
{this.props.extraButtons}
<CloseButton onClick={this.props.onClose} />
</div>
</div>
);
}
});
function Resizer({onMouseDown}) {
let [ref, setRef] = React.useState(null);
function onMouseDown_(event) {
let {x, y} = relativePosition(event, ref.getBoundingClientRect());
onMouseDown(event, x, y);
event.stopPropagation();
}
return (
<div className="resizer" ref={setRef} onMouseDown={onMouseDown_}>
<div className="arrow" />
</div>
);
}
module.exports = function Window(props) {
return (
<div ref={props.elementRef} className={classnames("window", {active: props.isActive})} style={props.style} onMouseDown={props.onMouseDown}>
<TitleBar title="Window Title Goes Here" onMouseDown={props.onTitleMouseDown} onClose={props.onClose} />
<div className="body">
<div className="contents">
{props.children}
</div>
{renderIf(props.resizable, <Resizer onMouseDown={props.onResizerMouseDown} />)}
</div>
</div>
);
};

@ -0,0 +1,26 @@
"use strict";
const createEventEmitter = require("create-event-emitter");
let eventTypes = [
"task:new",
"task:progress",
"task:completed"
];
module.exports = function createSseChannelClient() {
if (window.EventSource != null) {
let source = new EventSource("/event-stream");
let emitter = createEventEmitter();
eventTypes.forEach((type) => {
source.addEventListener(type, (event) => {
emitter.emit(type, JSON.parse(event.data));
});
});
return emitter;
} else {
throw new Error("This browser does not support SSE; could not establish event stream.");
}
};

@ -0,0 +1,5 @@
"use strict";
const React = require("react");
// TODO

@ -0,0 +1,181 @@
"use strict";
const Promise = require("bluebird");
const pathToRegexp = require("path-to-regexp");
const defaultValue = require("default-value");
const url = require("url");
const objectToFormData = require("../util/form-data/object-to-formdata");
const objectToURLSearchParams = require("../util/form-data/object-to-urlsearchparams");
module.exports = function createRouter(options = {}) {
let baseUrl = defaultValue(options.baseBackendUrl, "");
let router = {
_routes: [],
_getRoute: function getRoute(method, path) {
let matches;
let matchingRoute = router._routes.find((route) => route.method === method && (matches = route.regex.exec(path)));
if (matchingRoute == null) {
throw new Error(`No matching routes found for ${method.toUpperCase()} ${path}`);
} else {
let params = {};
matchingRoute.keys.forEach((key, i) => {
params[key] = matches[i + 1];
});
return {
handler: matchingRoute.handler,
params: params
};
}
},
get: function (...args) {
return this.addRoute("get", ...args);
},
post: function (...args) {
return this.addRoute("post", ...args);
},
put: function (...args) {
return this.addRoute("put", ...args);
},
delete: function (...args) {
return this.addRoute("delete", ...args);
},
head: function (...args) {
return this.addRoute("head", ...args);
},
patch: function (...args) {
return this.addRoute("patch", ...args);
},
addRoute: function addRoute(method, path, handler) {
/* Mutable arguments? WTF. */
let keys = [];
let regex = pathToRegexp(path, keys);
router._routes.push({ method, path, regex, keys, handler });
},
handle: function handleRequest(method, uri, data, handleOptions = {}) {
return Promise.try(() => {
/* TODO: Support relative paths? */
let {pathname, query} = url.parse(uri, true);
let route = this._getRoute(method, pathname);
let tasks = [];
let renderTaskAdded = false;
let req = {
path: pathname,
query: query,
body: data,
params: route.params,
pass: function(options = {}) {
return Promise.try(() => {
let body;
if (handleOptions.multipart) {
body = objectToFormData(this.body);
} else {
body = objectToURLSearchParams(this.body);
}
return window.fetch(url.resolve(baseUrl, uri), Object.assign({ // FIXME: Override URI but maintain query?
method: method,
credentials: "same-origin", /* FIXME: Allow sending them elsewhere as well? */
body: body
}, options));
}).then((response) => {
if (!response.ok) {
/* TODO: Is this what we want? */
throw new Error(`Got a non-200 response: ${response.status}`, {response: response});
} else {
return Promise.try(() => {
return response.json();
}).then((json) => {
return {
status: response.status,
body: json
};
});
}
});
},
/* TODO: passActions? */
passRender: function(viewName, options = {}) {
return Promise.try(() => {
return this.pass(options.requestOptions);
}).then((response) => {
let locals = defaultValue(options.locals, {});
let combinedLocals = Object.assign({}, locals, response.body);
res.render(viewName, combinedLocals, options.renderOptions);
});
}
};
let res = {
render: function(view, locals = {}, options = {}) {
if (renderTaskAdded === false) {
renderTaskAdded = true;
tasks.push({
type: "render",
view, locals, options
});
} else {
throw new Error("Can only render a view once per response");
}
},
redirect: function(path, redirectMethod) {
tasks.push({
type: "redirect",
path: path,
method: defaultValue(redirectMethod, method)
});
},
open: function(path, options = {}) {
tasks.push({
type: "open",
path, options
});
},
close: function(options = {}) {
tasks.push({
type: "close",
options
});
},
notify: function(message, options = {}) {
tasks.push({
type: "notify",
message: message,
options: {
type: defaultValue(options.type, "info"),
timeout: options.timeout,
title: options.title
}
});
},
error: function(error, context = {}) {
tasks.push({
type: "error",
error, context
});
}
};
return Promise.try(() => {
return route.handler(req, res);
}).then((result) => {
return {
result: result,
actions: tasks
};
});
});
}
};
return router;
};

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save