mirror of
https://github.com/laurent22/joplin.git
synced 2024-12-21 09:38:01 +02:00
Web app update
This commit is contained in:
parent
6c3c9d8a83
commit
d47bcdff64
15
.gitignore
vendored
15
.gitignore
vendored
@ -15,12 +15,13 @@
|
|||||||
/vendor/
|
/vendor/
|
||||||
/web/bundles/
|
/web/bundles/
|
||||||
*.sublime-workspace
|
*.sublime-workspace
|
||||||
database.sqlite
|
Makefile.Debug
|
||||||
QtClient/build-*-Debug/
|
Makefile.Release
|
||||||
*.pro.user
|
|
||||||
notes*.sqlite
|
|
||||||
Makefile
|
Makefile
|
||||||
Makefile.*
|
QtClient/build-*
|
||||||
TODO.md
|
TODO.md
|
||||||
tests/generated
|
*.pro.user
|
||||||
QtClient/JoplinQtClient/make.bat
|
QtClient/data/
|
||||||
|
app/data/uploads/
|
||||||
|
!app/data/uploads/.gitkeep
|
||||||
|
sparse_test.php
|
17
QtClient/JoplinQtClient/make.bat
Executable file
17
QtClient/JoplinQtClient/make.bat
Executable file
@ -0,0 +1,17 @@
|
|||||||
|
SET PATH=%PATH%;"C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\bin"
|
||||||
|
|
||||||
|
cd "D:\Web\www\joplin\QtClient\build-evernote-import-qt-Visual_C_32_bites-Debug"
|
||||||
|
if %errorlevel% neq 0 exit /b %errorlevel%
|
||||||
|
|
||||||
|
"C:\Qt\5.7\msvc2015\bin\qmake.exe" D:\Web\www\joplin\QtClient\evernote-import\evernote-import-qt.pro -spec win32-msvc2015 "CONFIG+=debug" "CONFIG+=qml_debug"
|
||||||
|
if %errorlevel% neq 0 exit /b %errorlevel%
|
||||||
|
|
||||||
|
"C:\Qt\Tools\QtCreator\bin\jom.exe" qmake_all
|
||||||
|
if %errorlevel% neq 0 exit /b %errorlevel%
|
||||||
|
|
||||||
|
"C:\Qt\Tools\QtCreator\bin\jom.exe"
|
||||||
|
if %errorlevel% neq 0 exit /b %errorlevel%
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/cygdrive/c/Qt/Tools/QtCreator/bin/jom.exe
|
@ -58,4 +58,15 @@ CREATE TABLE version (
|
|||||||
version INT
|
version INT
|
||||||
);
|
);
|
||||||
|
|
||||||
|
--CREATE TABLE mimetypes (
|
||||||
|
-- id INT,
|
||||||
|
-- mime TEXT
|
||||||
|
--);
|
||||||
|
|
||||||
|
--CREATE TABLE mimetype_extensions (
|
||||||
|
-- id INTEGER PRIMARY KEY,
|
||||||
|
-- mimetype_id,
|
||||||
|
-- extension TEXT
|
||||||
|
--);
|
||||||
|
|
||||||
INSERT INTO version (version) VALUES (1);
|
INSERT INTO version (version) VALUES (1);
|
||||||
|
@ -10,6 +10,7 @@
|
|||||||
#include <QSqlRecord>
|
#include <QSqlRecord>
|
||||||
#include <QCryptographicHash>
|
#include <QCryptographicHash>
|
||||||
#include <QTextCodec>
|
#include <QTextCodec>
|
||||||
|
#include <QDataStream>
|
||||||
|
|
||||||
#include "xmltomd.h"
|
#include "xmltomd.h"
|
||||||
|
|
||||||
@ -164,7 +165,18 @@ xmltomd::Resource parseResource(QXmlStreamReader& reader) {
|
|||||||
return xmltomd::Resource();
|
return xmltomd::Resource();
|
||||||
}
|
}
|
||||||
|
|
||||||
output.data = QByteArray::fromBase64(reader.readElementText().toUtf8());
|
|
||||||
|
//qApp->exit(0);
|
||||||
|
|
||||||
|
QByteArray ba;
|
||||||
|
// qDebug() << reader.text();
|
||||||
|
// qApp->exit(0);
|
||||||
|
QString s = reader.readElementText();
|
||||||
|
s = s.replace("\n", "");
|
||||||
|
ba.append(s);
|
||||||
|
output.data = QByteArray::fromBase64(ba);
|
||||||
|
// qDebug() << output.data.toBase64();
|
||||||
|
// exit(0);
|
||||||
} else if (reader.name() == "mime") {
|
} else if (reader.name() == "mime") {
|
||||||
output.mime = reader.readElementText();
|
output.mime = reader.readElementText();
|
||||||
} else if (reader.name() == "resource-attributes") {
|
} else if (reader.name() == "resource-attributes") {
|
||||||
@ -282,7 +294,7 @@ Note parseNote(QXmlStreamReader& reader) {
|
|||||||
// </en-export>
|
// </en-export>
|
||||||
|
|
||||||
int mediaHashIndex = 0;
|
int mediaHashIndex = 0;
|
||||||
for (int i = 0; i < note.resources.size(); i++) {
|
for (size_t i = 0; i < note.resources.size(); i++) {
|
||||||
xmltomd::Resource& r = note.resources[i];
|
xmltomd::Resource& r = note.resources[i];
|
||||||
if (r.id == "") {
|
if (r.id == "") {
|
||||||
if (note.enMediaElements.size() <= mediaHashIndex) {
|
if (note.enMediaElements.size() <= mediaHashIndex) {
|
||||||
@ -339,6 +351,13 @@ void filePutContents(const QString& filePath, const QString& content) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QString extensionFromMimeType(const QString& mimeType) {
|
||||||
|
if (mimeType == "image/jpg" || mimeType == "image/jpeg") return ".jpg";
|
||||||
|
if (mimeType == "image/png") return ".png";
|
||||||
|
if (mimeType == "image/gif") return ".gif";
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
int main(int argc, char *argv[]) {
|
int main(int argc, char *argv[]) {
|
||||||
QCoreApplication a(argc, argv);
|
QCoreApplication a(argc, argv);
|
||||||
|
|
||||||
@ -387,9 +406,9 @@ int main(int argc, char *argv[]) {
|
|||||||
|
|
||||||
std::vector<Note> notes = parseXmlFile(fileInfo.absoluteFilePath());
|
std::vector<Note> notes = parseXmlFile(fileInfo.absoluteFilePath());
|
||||||
|
|
||||||
for (int noteIndex = 0; noteIndex < notes.size(); noteIndex++) {
|
for (size_t noteIndex = 0; noteIndex < notes.size(); noteIndex++) {
|
||||||
Note n = notes[noteIndex];
|
Note n = notes[noteIndex];
|
||||||
for (int resourceIndex = 0; resourceIndex < n.resources.size(); resourceIndex++) {
|
for (size_t resourceIndex = 0; resourceIndex < n.resources.size(); resourceIndex++) {
|
||||||
xmltomd::Resource resource = n.resources[resourceIndex];
|
xmltomd::Resource resource = n.resources[resourceIndex];
|
||||||
QSqlQuery query(db);
|
QSqlQuery query(db);
|
||||||
query.prepare("INSERT INTO resources (id, title, mime, filename, created_time, updated_time) VALUES (?,?,?,?,?,?)");
|
query.prepare("INSERT INTO resources (id, title, mime, filename, created_time, updated_time) VALUES (?,?,?,?,?,?)");
|
||||||
@ -406,10 +425,19 @@ int main(int argc, char *argv[]) {
|
|||||||
query.addBindValue(resource.id);
|
query.addBindValue(resource.id);
|
||||||
query.addBindValue(n.id);
|
query.addBindValue(n.id);
|
||||||
query.exec();
|
query.exec();
|
||||||
|
|
||||||
|
QString resourceFilePath = resourceDir + "/" + resource.id; //+ extensionFromMimeType(resource.mime);
|
||||||
|
QFile resourceFile(resourceFilePath);
|
||||||
|
if (resourceFile.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
|
||||||
|
QDataStream stream(&resourceFile);
|
||||||
|
stream << resource.data;
|
||||||
|
} else {
|
||||||
|
qWarning() << "Cannot write to" << resourceFilePath;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (int noteIndex = 0; noteIndex < notes.size(); noteIndex++) {
|
for (size_t noteIndex = 0; noteIndex < notes.size(); noteIndex++) {
|
||||||
Note n = notes[noteIndex];
|
Note n = notes[noteIndex];
|
||||||
|
|
||||||
// if (i != 8 || noteIndex != 3090) continue;
|
// if (i != 8 || noteIndex != 3090) continue;
|
||||||
|
@ -7,11 +7,15 @@ services:
|
|||||||
|
|
||||||
app.eloquent:
|
app.eloquent:
|
||||||
class: AppBundle\Eloquent
|
class: AppBundle\Eloquent
|
||||||
arguments: []
|
arguments: ['@app.mime_types', '@app.paths']
|
||||||
|
|
||||||
# app.fine_diff:
|
|
||||||
# class: AppBundle\FineDiff
|
|
||||||
# arguments: []
|
|
||||||
|
|
||||||
twig.exception_listener:
|
twig.exception_listener:
|
||||||
class: stdObject
|
class: stdObject
|
||||||
|
|
||||||
|
app.paths:
|
||||||
|
class: AppBundle\Paths
|
||||||
|
arguments: [%kernel.root_dir%]
|
||||||
|
|
||||||
|
app.mime_types:
|
||||||
|
class: AppBundle\MimeTypes
|
||||||
|
arguments: ['@app.paths']
|
||||||
|
1853
app/data/mime.types
Executable file
1853
app/data/mime.types
Executable file
File diff suppressed because it is too large
Load Diff
1
app/data/mime_types.php
Executable file
1
app/data/mime_types.php
Executable file
File diff suppressed because one or more lines are too long
0
app/data/uploads/.gitkeep
Normal file
0
app/data/uploads/.gitkeep
Normal file
@ -140,7 +140,7 @@ class Config {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class FolderItem {
|
class BaseItem {
|
||||||
|
|
||||||
private $title = '';
|
private $title = '';
|
||||||
private $body = '';
|
private $body = '';
|
||||||
@ -183,17 +183,17 @@ class FolderItem {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class FolderItems {
|
class BaseItems {
|
||||||
|
|
||||||
private $items = array();
|
private $items = array();
|
||||||
|
|
||||||
private function getFolderItems($dir, $parentId, &$output) {
|
private function getBaseItems($dir, $parentId, &$output) {
|
||||||
$paths = glob($dir . '/*');
|
$paths = glob($dir . '/*');
|
||||||
foreach ($paths as $path) {
|
foreach ($paths as $path) {
|
||||||
$isFolder = is_dir($path);
|
$isFolder = is_dir($path);
|
||||||
$modTime = filemtime($path);
|
$modTime = filemtime($path);
|
||||||
|
|
||||||
$o = new FolderItem();
|
$o = new BaseItem();
|
||||||
$o->setTitle(basename($path));
|
$o->setTitle(basename($path));
|
||||||
$o->setId(Api::createId($parentId . '_' . $o->title()));
|
$o->setId(Api::createId($parentId . '_' . $o->title()));
|
||||||
$o->setParentId($parentId);
|
$o->setParentId($parentId);
|
||||||
@ -202,13 +202,13 @@ class FolderItems {
|
|||||||
|
|
||||||
if (!$isFolder) $o->setBody(file_get_contents($path));
|
if (!$isFolder) $o->setBody(file_get_contents($path));
|
||||||
$output[] = $o;
|
$output[] = $o;
|
||||||
if ($isFolder) $this->getFolderItems($path, $o->id(), $output);
|
if ($isFolder) $this->getBaseItems($path, $o->id(), $output);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function fromPath($path) {
|
public function fromPath($path) {
|
||||||
$this->items = array();
|
$this->items = array();
|
||||||
$this->getFolderItems($path, null, $this->items);
|
$this->getBaseItems($path, null, $this->items);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function all() {
|
public function all() {
|
||||||
@ -272,8 +272,8 @@ $api->setSessionId($session['id']);
|
|||||||
if (array_key_exists('sync', $flags)) {
|
if (array_key_exists('sync', $flags)) {
|
||||||
$syncStartTime = time();
|
$syncStartTime = time();
|
||||||
$lastSyncTime = $config->get('last_sync_time');
|
$lastSyncTime = $config->get('last_sync_time');
|
||||||
$folderItems = new FolderItems();
|
$BaseItems = new BaseItems();
|
||||||
$folderItems->fromPath($dataPath);
|
$BaseItems->fromPath($dataPath);
|
||||||
|
|
||||||
// ------------------------------------------------------------------------------------------
|
// ------------------------------------------------------------------------------------------
|
||||||
// Get latest changes from API
|
// Get latest changes from API
|
||||||
@ -287,7 +287,7 @@ if (array_key_exists('sync', $flags)) {
|
|||||||
$notes = array();
|
$notes = array();
|
||||||
$maxId = null;
|
$maxId = null;
|
||||||
foreach ($response['items'] as $item) {
|
foreach ($response['items'] as $item) {
|
||||||
$folderItem = new FolderItem();
|
$BaseItem = new BaseItem();
|
||||||
|
|
||||||
switch ($item['type']) {
|
switch ($item['type']) {
|
||||||
|
|
||||||
@ -295,7 +295,7 @@ if (array_key_exists('sync', $flags)) {
|
|||||||
case 'update':
|
case 'update':
|
||||||
|
|
||||||
$resource = $api->exec('GET', $item['item_type'] . 's/' . $item['item_id']);
|
$resource = $api->exec('GET', $item['item_type'] . 's/' . $item['item_id']);
|
||||||
$folderItem->fromApiArray($item['item_type'], $resource);
|
$BaseItem->fromApiArray($item['item_type'], $resource);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@ -304,13 +304,13 @@ if (array_key_exists('sync', $flags)) {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$folderItems->setById($folderItem->id(), $folderItem);
|
$BaseItems->setById($BaseItem->id(), $BaseItem);
|
||||||
|
|
||||||
$maxId = max($item['id'], $maxId);
|
$maxId = max($item['id'], $maxId);
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ($folderItems->all() as $item) {
|
foreach ($BaseItems->all() as $item) {
|
||||||
$relativePath = $folderItems->itemFullPath($item);
|
$relativePath = $BaseItems->itemFullPath($item);
|
||||||
$path = $dataPath . '/' . $relativePath;
|
$path = $dataPath . '/' . $relativePath;
|
||||||
|
|
||||||
foreach (array('folder', 'note') as $itemType) {
|
foreach (array('folder', 'note') as $itemType) {
|
||||||
@ -333,7 +333,7 @@ if (array_key_exists('sync', $flags)) {
|
|||||||
// Send changed notes and folders to API
|
// Send changed notes and folders to API
|
||||||
// ------------------------------------------------------------------------------------------
|
// ------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
foreach ($folderItems->all() as $item) {
|
foreach ($BaseItems->all() as $item) {
|
||||||
if ($item->modTime() < $lastSyncTime) continue;
|
if ($item->modTime() < $lastSyncTime) continue;
|
||||||
|
|
||||||
if ($item->isFolder()) {
|
if ($item->isFolder()) {
|
||||||
|
12
spa_client/.babelrc
Executable file
12
spa_client/.babelrc
Executable file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"presets": [
|
||||||
|
"es2015",
|
||||||
|
"react",
|
||||||
|
"stage-0"
|
||||||
|
],
|
||||||
|
"plugins": [
|
||||||
|
[
|
||||||
|
"transform-decorators-legacy"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
39
spa_client/.gitignore
vendored
Executable file
39
spa_client/.gitignore
vendored
Executable file
@ -0,0 +1,39 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
pids
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
|
||||||
|
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||||
|
lib-cov
|
||||||
|
|
||||||
|
# Coverage directory used by tools like istanbul
|
||||||
|
coverage
|
||||||
|
|
||||||
|
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||||
|
.grunt
|
||||||
|
|
||||||
|
# node-waf configuration
|
||||||
|
.lock-wscript
|
||||||
|
|
||||||
|
# Compiled binary addons (http://nodejs.org/api/addons.html)
|
||||||
|
build/Release
|
||||||
|
|
||||||
|
# Dependency directory
|
||||||
|
node_modules
|
||||||
|
|
||||||
|
# Optional npm cache directory
|
||||||
|
.npm
|
||||||
|
|
||||||
|
# Optional REPL history
|
||||||
|
.node_repl_history
|
||||||
|
|
||||||
|
# Ignore files for distribution
|
||||||
|
dist/*
|
||||||
|
|
||||||
|
# OSX .DS_Store files
|
||||||
|
.DS_Store
|
19
spa_client/README.md
Executable file
19
spa_client/README.md
Executable file
@ -0,0 +1,19 @@
|
|||||||
|
React/Redux Example Todo Application
|
||||||
|
====================================
|
||||||
|
|
||||||
|
An example application demonstrating how to build a Todo with React and Redux.
|
||||||
|
|
||||||
|
# How to run
|
||||||
|
|
||||||
|
Open up a terminal and execute:
|
||||||
|
|
||||||
|
```term
|
||||||
|
$ npm install
|
||||||
|
$ npm start
|
||||||
|
```
|
||||||
|
|
||||||
|
Then open up your browser and navigate to [http://localhost:3000/](http://localhost:3000/).
|
||||||
|
|
||||||
|
# License
|
||||||
|
|
||||||
|
Apache 2.0
|
42
spa_client/package.json
Executable file
42
spa_client/package.json
Executable file
@ -0,0 +1,42 @@
|
|||||||
|
{
|
||||||
|
"name": "stormpath-react-redux-todo-example-application",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"description": "React/Redux example todo application.",
|
||||||
|
"homepage": "https://github.com/typerandom/stormpath-react-redux-todo-example-application",
|
||||||
|
"author": "Stormpath, 2016",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
|
"dev": "node ./server ./src/ ./webpack.dev.config",
|
||||||
|
"build": "rm -rf ./dist/ && mkdir ./dist/ && cp -r ./src/* ./dist && rm -rf ./dist/js/* && webpack",
|
||||||
|
"start": "npm run build && node ./server"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/typerandom/stormpath-react-redux-todo-example-application.git"
|
||||||
|
},
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/typerandom/stormpath-react-redux-todo-example-application/issues"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"babel-core": "^6.3.26",
|
||||||
|
"babel-loader": "^6.2.0",
|
||||||
|
"babel-plugin-react-transform": "^2.0.0",
|
||||||
|
"babel-plugin-transform-decorators-legacy": "^1.3.4",
|
||||||
|
"babel-preset-es2015": "^6.3.13",
|
||||||
|
"babel-preset-react": "^6.3.13",
|
||||||
|
"babel-preset-stage-0": "^6.3.13",
|
||||||
|
"babel-runtime": "^6.3.19",
|
||||||
|
"express": "^4.13.4",
|
||||||
|
"morgan": "^1.7.0",
|
||||||
|
"open": "0.0.5",
|
||||||
|
"react": "^0.14.7",
|
||||||
|
"react-dom": "^0.14.7",
|
||||||
|
"react-redux": "^4.4.6",
|
||||||
|
"redux": "^3.4.0",
|
||||||
|
"webpack": "^1.12.13",
|
||||||
|
"webpack-dev-middleware": "^1.5.1",
|
||||||
|
"object-path-immutable": "^0.5.1",
|
||||||
|
"deepcopy": "^0.6.3"
|
||||||
|
}
|
||||||
|
}
|
32
spa_client/server.js
Executable file
32
spa_client/server.js
Executable file
@ -0,0 +1,32 @@
|
|||||||
|
var open = require('open');
|
||||||
|
var path = require('path');
|
||||||
|
var morgan = require('morgan');
|
||||||
|
var express = require('express');
|
||||||
|
var webpack = require('webpack');
|
||||||
|
var webpackDevMiddleware = require('webpack-dev-middleware');
|
||||||
|
|
||||||
|
var config = require('./webpack.config');
|
||||||
|
var compiler = webpack(config);
|
||||||
|
|
||||||
|
var app = express();
|
||||||
|
|
||||||
|
app.use(morgan('dev'));
|
||||||
|
|
||||||
|
app.use(webpackDevMiddleware(compiler, {
|
||||||
|
noInfo: true,
|
||||||
|
publicPath: config.output.publicPath
|
||||||
|
}));
|
||||||
|
|
||||||
|
app.use(express.static('./dist/'));
|
||||||
|
|
||||||
|
app.get('*', function (req, res){
|
||||||
|
res.sendFile(path.resolve(__dirname, './dist/', 'index.html'))
|
||||||
|
});
|
||||||
|
|
||||||
|
app.listen(3000, function (err) {
|
||||||
|
if (err) {
|
||||||
|
return console.error(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Web listening at http://localhost:3000/.');
|
||||||
|
});
|
39
spa_client/src/index.html
Executable file
39
spa_client/src/index.html
Executable file
@ -0,0 +1,39 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<title></title>
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<style>
|
||||||
|
a, a:active, a:visited {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.selected {
|
||||||
|
border: 1px solid black;
|
||||||
|
}
|
||||||
|
.level-1 {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
.level-2 {
|
||||||
|
margin-left: 20px;
|
||||||
|
}
|
||||||
|
.level-3 {
|
||||||
|
margin-left: 40px;
|
||||||
|
}
|
||||||
|
.folder-list {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: top;
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
.note-list {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: top;
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="container"></div>
|
||||||
|
<script src="/js/app.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
420
spa_client/src/js/app.jsx
Executable file
420
spa_client/src/js/app.jsx
Executable file
@ -0,0 +1,420 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { render } from 'react-dom'
|
||||||
|
import { createStore } from 'redux';
|
||||||
|
import { Provider } from 'react-redux'
|
||||||
|
import RootFolderList from 'components/root-folder-list.jsx';
|
||||||
|
import NoteList from 'components/note-list.jsx';
|
||||||
|
import { reducer } from './reducer.jsx'
|
||||||
|
|
||||||
|
let defaultState = {
|
||||||
|
'myButtonLabel': 'click',
|
||||||
|
items: [
|
||||||
|
{ id: 101, title: 'folder 1', type: 1, parent_id: 0 },
|
||||||
|
{ id: 102, title: 'folder 2', type: 1, parent_id: 0 },
|
||||||
|
{ id: 103, title: 'folder 3', type: 1, parent_id: 101 },
|
||||||
|
{ id: 104, title: 'folder 4', type: 1, parent_id: 101 },
|
||||||
|
{ id: 105, title: 'folder 5', type: 1, parent_id: 0 },
|
||||||
|
{ id: 106, title: 'folder 6', type: 1, parent_id: 105 },
|
||||||
|
{ id: 1, type: 2, parent_id: 101, title: 'one', body: '111 dsqfdsmlk mqkfkdq sfkl qlmskfqm' },
|
||||||
|
{ id: 2, type: 2, parent_id: 101, title: 'two', body: '222 dsqfdsmlk mqkfkdq sfkl 222 qlmskfqm' },
|
||||||
|
{ id: 3, type: 2, parent_id: 103, title: 'three', body: '33 dsqfdsmlk mqkfkdq sfkl 33 qlmskfqm' },
|
||||||
|
{ id: 4, type: 2, parent_id: 103, title: 'four', body: '4222 dsqfdsmlk mqkfkdq sfkl 222 qlmskfqm' },
|
||||||
|
{ id: 5, type: 2, parent_id: 103, title: 'five', body: '5222 dsqfdsmlk mqkfkdq sfkl 222 qlmskfqm' },
|
||||||
|
{ id: 6, type: 2, parent_id: 104, title: 'six', body: '6222 dsqfdsmlk mqkfkdq sfkl 222 qlmskfqm' },
|
||||||
|
{ id: 7, type: 2, parent_id: 104, title: 'seven', body: '7222 dsqfdsmlk mqkfkdq sfkl 222 qlmskfqm' },
|
||||||
|
],
|
||||||
|
selectedFolderId: null,
|
||||||
|
selectedNoteId: null,
|
||||||
|
expandedFolderIds: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
let store = createStore(reducer, defaultState)
|
||||||
|
|
||||||
|
class App extends React.Component {
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<RootFolderList />
|
||||||
|
<NoteList />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
render(
|
||||||
|
<Provider store={store}>
|
||||||
|
<App />
|
||||||
|
</Provider>,
|
||||||
|
document.getElementById('container')
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
// var defaultState = {
|
||||||
|
// folders: [],
|
||||||
|
// todo: {
|
||||||
|
// items: []
|
||||||
|
// },
|
||||||
|
// selectedFolderId: null
|
||||||
|
// };
|
||||||
|
|
||||||
|
// function addTodo(message) {
|
||||||
|
// return {
|
||||||
|
// type: 'ADD_TODO',
|
||||||
|
// message: message,
|
||||||
|
// completed: false
|
||||||
|
// };
|
||||||
|
// }
|
||||||
|
|
||||||
|
// function completeTodo(index) {
|
||||||
|
// return {
|
||||||
|
// type: 'COMPLETE_TODO',
|
||||||
|
// index: index
|
||||||
|
// };
|
||||||
|
// }
|
||||||
|
|
||||||
|
// function deleteTodo(index) {
|
||||||
|
// return {
|
||||||
|
// type: 'DELETE_TODO',
|
||||||
|
// index: index
|
||||||
|
// };
|
||||||
|
// }
|
||||||
|
|
||||||
|
// function clearTodo() {
|
||||||
|
// return {
|
||||||
|
// type: 'CLEAR_TODO'
|
||||||
|
// };
|
||||||
|
// }
|
||||||
|
|
||||||
|
// function createId() {
|
||||||
|
// return Math.round(Math.random() * 99999);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// function folderIndex(state, id) {
|
||||||
|
// for (var i = 0; i < state.folders.length; i++) {
|
||||||
|
// if (state.folders[i].id == id) return i;
|
||||||
|
// }
|
||||||
|
// return -1;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// function folderById(state, id) {
|
||||||
|
// var i = folderIndex(state, id);
|
||||||
|
// return i >= 0 ? state.folders[i] : null;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// function todoApp(state, action) {
|
||||||
|
|
||||||
|
// switch (action.type) {
|
||||||
|
|
||||||
|
// case 'ADD_FOLDER':
|
||||||
|
|
||||||
|
// var folder = {
|
||||||
|
// name: action.name,
|
||||||
|
// id: createId(),
|
||||||
|
// selected: false
|
||||||
|
// };
|
||||||
|
|
||||||
|
// state = immutable.push(state, 'folders', folder);
|
||||||
|
// state = immutable.set(state, 'selectedFolderId', folder.id);
|
||||||
|
// return state;
|
||||||
|
|
||||||
|
// case 'DELETE_FOLDER':
|
||||||
|
|
||||||
|
// var folders = deepcopy(state.folders);
|
||||||
|
// var index = folderIndex(state, action.id);
|
||||||
|
// if (index < 0) return state;
|
||||||
|
// folders.splice(index, 1);
|
||||||
|
// return immutable.set(state, 'folders', folders);
|
||||||
|
|
||||||
|
// case 'SET_SELECTED_FOLDER':
|
||||||
|
|
||||||
|
// return immutable.set(state, 'selectedFolderId', action.id);
|
||||||
|
|
||||||
|
// case 'SET_FOLDER_NAME':
|
||||||
|
|
||||||
|
// var idx = folderIndex(state, action.id);
|
||||||
|
// return immutable.set(state, 'folders.' + idx + '.name', action.name);
|
||||||
|
|
||||||
|
// case 'ADD_TODO':
|
||||||
|
// var items = [].concat(state.todo.items);
|
||||||
|
// return Object.assign({}, state, {
|
||||||
|
// todo: {
|
||||||
|
// items: items.concat([{
|
||||||
|
// message: action.message,
|
||||||
|
// completed: false
|
||||||
|
// }])
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
|
||||||
|
// case 'COMPLETE_TODO':
|
||||||
|
// var items = [].concat(state.todo.items);
|
||||||
|
|
||||||
|
// items[action.index].completed = true;
|
||||||
|
|
||||||
|
// return Object.assign({}, state, {
|
||||||
|
// todo: {
|
||||||
|
// items: items
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
|
||||||
|
// case 'DELETE_TODO':
|
||||||
|
// var items = [].concat(state.todo.items);
|
||||||
|
|
||||||
|
// items.splice(action.index, 1);
|
||||||
|
|
||||||
|
// return Object.assign({}, state, {
|
||||||
|
// todo: {
|
||||||
|
// items: items
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
|
||||||
|
// case 'CLEAR_TODO':
|
||||||
|
// return Object.assign({}, state, {
|
||||||
|
// todo: {
|
||||||
|
// items: []
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
|
||||||
|
// default:
|
||||||
|
// return state;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// var store = createStore(todoApp, defaultState);
|
||||||
|
|
||||||
|
// class AddTodoForm extends React.Component {
|
||||||
|
// state = {
|
||||||
|
// message: ''
|
||||||
|
// };
|
||||||
|
|
||||||
|
// onFormSubmit(e) {
|
||||||
|
// e.preventDefault();
|
||||||
|
// store.dispatch(addTodo(this.state.message));
|
||||||
|
// this.setState({ message: '' });
|
||||||
|
// }
|
||||||
|
|
||||||
|
// onMessageChanged(e) {
|
||||||
|
// var message = e.target.value;
|
||||||
|
// this.setState({ message: message });
|
||||||
|
// }
|
||||||
|
|
||||||
|
// render() {
|
||||||
|
// return (
|
||||||
|
// <form onSubmit={this.onFormSubmit.bind(this)}>
|
||||||
|
// <input type="text" placeholder="Todo..." onChange={this.onMessageChanged.bind(this)} value={this.state.message} />
|
||||||
|
// <input type="submit" value="Add" />
|
||||||
|
// </form>
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// class AddFolderForm extends React.Component {
|
||||||
|
// state = {
|
||||||
|
// name: ''
|
||||||
|
// };
|
||||||
|
|
||||||
|
// onFormSubmit(e) {
|
||||||
|
// e.preventDefault();
|
||||||
|
// store.dispatch({
|
||||||
|
// type: 'ADD_FOLDER',
|
||||||
|
// name: this.state.name
|
||||||
|
// });
|
||||||
|
// this.setState({ name: '' });
|
||||||
|
// }
|
||||||
|
|
||||||
|
// onInputChange(e) {
|
||||||
|
// this.setState({ name: e.target.value });
|
||||||
|
// }
|
||||||
|
|
||||||
|
// render() {
|
||||||
|
// return (
|
||||||
|
// <form onSubmit={this.onFormSubmit.bind(this)}>
|
||||||
|
// <input type="text" placeholder="Folder..." onChange={this.onInputChange.bind(this)} value={this.state.name} />
|
||||||
|
// <input type="submit" value="Add" />
|
||||||
|
// </form>
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// class TodoItem extends React.Component {
|
||||||
|
// onDeleteClick() {
|
||||||
|
// store.dispatch(deleteTodo(this.props.index));
|
||||||
|
// }
|
||||||
|
|
||||||
|
// onCompletedClick() {
|
||||||
|
// store.dispatch(completeTodo(this.props.index));
|
||||||
|
// }
|
||||||
|
|
||||||
|
// render() {
|
||||||
|
// return (
|
||||||
|
// <li>
|
||||||
|
// <a href="#" onClick={this.onCompletedClick.bind(this)} style={{textDecoration: this.props.completed ? 'line-through' : 'none'}}>{this.props.message.trim()}</a>
|
||||||
|
// <a href="#" onClick={this.onDeleteClick.bind(this)} style={{textDecoration: 'none'}}>[x]</a>
|
||||||
|
// </li>
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// class FolderItem extends React.Component {
|
||||||
|
// onDeleteClick() {
|
||||||
|
// store.dispatch({
|
||||||
|
// type: 'DELETE_FOLDER',
|
||||||
|
// id: this.props.item.id
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
|
||||||
|
// onSelected() {
|
||||||
|
// store.dispatch({
|
||||||
|
// type: 'SET_SELECTED_FOLDER',
|
||||||
|
// id: this.props.item.id
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
|
||||||
|
// render() {
|
||||||
|
// let selectedClass = this.props.selected ? 'selected' : '';
|
||||||
|
// return (
|
||||||
|
// <li>
|
||||||
|
// <a href="#" className={selectedClass} onClick={this.onSelected.bind(this)}>{this.props.item.name} ({this.props.item.id})</a> <a href="#" onClick={this.onDeleteClick.bind(this)}>[x]</a>
|
||||||
|
// </li>
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// class FolderList extends React.Component {
|
||||||
|
// state = {
|
||||||
|
// folders: [],
|
||||||
|
// selectedFolderId: null,
|
||||||
|
// folderName: '',
|
||||||
|
// lastSelectedFolderId: null
|
||||||
|
// };
|
||||||
|
|
||||||
|
// componentWillMount() {
|
||||||
|
// store.subscribe(() => {
|
||||||
|
// var state = store.getState();
|
||||||
|
// this.setState({
|
||||||
|
// folders: state.folders,
|
||||||
|
// selectedFolderId: state.selectedFolderId
|
||||||
|
// });
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
|
||||||
|
// folderNameInput_keyPress(e) {
|
||||||
|
// if (e.key == 'Enter') {
|
||||||
|
// console.info(this.state.folderName);
|
||||||
|
// store.dispatch({
|
||||||
|
// type: 'SET_FOLDER_NAME',
|
||||||
|
// name: this.state.folderName,
|
||||||
|
// id: this.state.selectedFolderId
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// folderNameInput_onChange(e) {
|
||||||
|
// this.setState({ folderName: e.target.value });
|
||||||
|
// }
|
||||||
|
|
||||||
|
// render() {
|
||||||
|
// var items = [];
|
||||||
|
|
||||||
|
// this.state.folders.forEach((item, index) => {
|
||||||
|
// let isSelected = this.state.selectedFolderId == item.id;
|
||||||
|
// items.push(
|
||||||
|
// <FolderItem
|
||||||
|
// key={index}
|
||||||
|
// index={index}
|
||||||
|
// item={item}
|
||||||
|
// selected={isSelected} />
|
||||||
|
// );
|
||||||
|
// });
|
||||||
|
|
||||||
|
// if (!items.length) {
|
||||||
|
// return (
|
||||||
|
// <p>
|
||||||
|
// <i>No folder.</i>
|
||||||
|
// </p>
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
// var selectedFolder = folderById(this.state, this.state.selectedFolderId);
|
||||||
|
// var selectedFolderId = selectedFolder ? selectedFolder.id : null;
|
||||||
|
// if (selectedFolderId !== this.state.lastSelectedFolderId) {
|
||||||
|
// this.state.folderName = selectedFolder ? selectedFolder.name : '';
|
||||||
|
// this.state.lastSelectedFolderId = selectedFolderId;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return (
|
||||||
|
// <div>
|
||||||
|
// <ol>{ items }</ol>
|
||||||
|
// <input type="text" onKeyPress={this.folderNameInput_keyPress.bind(this)} onChange={this.folderNameInput_onChange.bind(this)} value={this.state.folderName} />
|
||||||
|
// </div>
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
// class TodoList extends React.Component {
|
||||||
|
// state = {
|
||||||
|
// items: []
|
||||||
|
// };
|
||||||
|
|
||||||
|
// componentWillMount() {
|
||||||
|
// store.subscribe(() => {
|
||||||
|
// var state = store.getState();
|
||||||
|
// this.setState({
|
||||||
|
// items: state.todo.items
|
||||||
|
// });
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
|
||||||
|
// render() {
|
||||||
|
// var items = [];
|
||||||
|
|
||||||
|
// this.state.items.forEach((item, index) => {
|
||||||
|
// items.push(<TodoItem
|
||||||
|
// key={index}
|
||||||
|
// index={index}
|
||||||
|
// message={item.message}
|
||||||
|
// completed={item.completed}
|
||||||
|
// />);
|
||||||
|
// });
|
||||||
|
|
||||||
|
// if (!items.length) {
|
||||||
|
// return (
|
||||||
|
// <p>
|
||||||
|
// <i>Please add something to do.</i>
|
||||||
|
// </p>
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return (
|
||||||
|
// <ol>{ items }</ol>
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// ReactDOM.render(
|
||||||
|
// <div>
|
||||||
|
// <h1>Todo</h1>
|
||||||
|
// <AddTodoForm />
|
||||||
|
// <AddFolderForm />
|
||||||
|
// <FolderList />
|
||||||
|
// <TodoList />
|
||||||
|
// </div>,
|
||||||
|
// document.getElementById('container')
|
||||||
|
// );
|
||||||
|
|
||||||
|
// store.dispatch({
|
||||||
|
// type: 'ADD_FOLDER',
|
||||||
|
// name: 'aaaa'
|
||||||
|
// });
|
||||||
|
|
||||||
|
// store.dispatch({
|
||||||
|
// type: 'ADD_FOLDER',
|
||||||
|
// name: 'bbbb'
|
||||||
|
// });
|
||||||
|
|
||||||
|
// store.dispatch({
|
||||||
|
// type: 'ADD_FOLDER',
|
||||||
|
// name: 'cccc'
|
||||||
|
// });
|
39
spa_client/src/js/components/folder-list.jsx
Executable file
39
spa_client/src/js/components/folder-list.jsx
Executable file
@ -0,0 +1,39 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import Folder from './folder.jsx';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import * as fi from 'models/folder-item.jsx';
|
||||||
|
|
||||||
|
class FolderListComponent extends React.Component {
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let elements = [];
|
||||||
|
let level = Number(this.props.level) + 1;
|
||||||
|
let className = 'level-' + level;
|
||||||
|
className += ' folder-list';
|
||||||
|
|
||||||
|
this.props.items.forEach((item, index) => {
|
||||||
|
if (this.props.parentId != item.parent_id) return;
|
||||||
|
let selected = this.props.selectedFolderId == item.id;
|
||||||
|
let children = fi.children(this.props.items, item.id);
|
||||||
|
elements.push(<Folder level={level} title={item.title} key={item.id} id={item.id} expandedFolderIds={this.props.expandedFolderIds} selectedFolderId={this.props.selectedFolderId} children={children} />);
|
||||||
|
});
|
||||||
|
|
||||||
|
return <div className={className}>{elements}</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapStateToProps = function(state) {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = function(dispatch) {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const FolderList = connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(FolderListComponent)
|
||||||
|
|
||||||
|
export default FolderList
|
50
spa_client/src/js/components/folder.jsx
Executable file
50
spa_client/src/js/components/folder.jsx
Executable file
@ -0,0 +1,50 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { connect } from 'react-redux'
|
||||||
|
import FolderList from './folder-list.jsx';
|
||||||
|
|
||||||
|
class FolderComponent extends React.Component {
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let selectedClass = this.props.selectedFolderId == this.props.id ? 'selected' : '';
|
||||||
|
let elements = [];
|
||||||
|
let key = 'note-name-' + this.props.id;
|
||||||
|
|
||||||
|
elements.push(
|
||||||
|
<div key={key} onClick={this.props.onClick.bind(this)} className={selectedClass} id="{this.props.id}">{this.props.title}</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
var showChildren = this.props.children.length && this.props.expandedFolderIds.indexOf(this.props.id) >= 0;
|
||||||
|
|
||||||
|
if (showChildren) {
|
||||||
|
key = 'folder-list-' + this.props.id;
|
||||||
|
elements.push(
|
||||||
|
<FolderList key={key} level={this.props.level} parentId={this.props.id} items={this.props.children} selectedFolderId={this.props.selectedFolderId} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div>{elements}</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const Folder = connect(
|
||||||
|
function(state) { return {} },
|
||||||
|
|
||||||
|
function(dispatch) {
|
||||||
|
return {
|
||||||
|
onClick: function(event) {
|
||||||
|
dispatch({
|
||||||
|
type: 'SELECT_FOLDER',
|
||||||
|
id: this.props.id,
|
||||||
|
});
|
||||||
|
dispatch({
|
||||||
|
type: 'TOGGLE_FOLDER',
|
||||||
|
id: this.props.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
)(FolderComponent)
|
||||||
|
|
||||||
|
export default Folder
|
11
spa_client/src/js/components/my-button.js
Executable file
11
spa_client/src/js/components/my-button.js
Executable file
@ -0,0 +1,11 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
class MyButton extends React.Component {
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return <button onClick={this.props.onClick}>{this.props.label}</button>
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MyButton
|
28
spa_client/src/js/components/note-list-item.jsx
Executable file
28
spa_client/src/js/components/note-list-item.jsx
Executable file
@ -0,0 +1,28 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { connect } from 'react-redux'
|
||||||
|
|
||||||
|
class NoteListItemComponent extends React.Component {
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let className = this.props.selected ? 'selected' : '';
|
||||||
|
return <div onClick={this.props.onClick.bind(this)} className={className}>{this.props.title}</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const NoteListItem = connect(
|
||||||
|
function(state) { return {} },
|
||||||
|
function(dispatch) {
|
||||||
|
return {
|
||||||
|
onClick: function(event) {
|
||||||
|
console.info(this.props.id);
|
||||||
|
dispatch({
|
||||||
|
type: 'SELECT_NOTE',
|
||||||
|
id: this.props.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)(NoteListItemComponent)
|
||||||
|
|
||||||
|
export default NoteListItem
|
37
spa_client/src/js/components/note-list.jsx
Executable file
37
spa_client/src/js/components/note-list.jsx
Executable file
@ -0,0 +1,37 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import NoteListItem from './note-list-item.jsx';
|
||||||
|
import { connect } from 'react-redux'
|
||||||
|
import * as fi from 'models/folder-item.jsx';
|
||||||
|
|
||||||
|
class NoteListComponent extends React.Component {
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let elements = [];
|
||||||
|
|
||||||
|
this.props.items.forEach((item, index) => {
|
||||||
|
let selected = this.props.selectedNoteId == item.id;
|
||||||
|
elements.push(<NoteListItem selected={selected} key={item.id} title={item.title} body={item.body} id={item.id} />);
|
||||||
|
});
|
||||||
|
|
||||||
|
return <div className="note-list">{elements}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapStateToProps = function(state) {
|
||||||
|
return {
|
||||||
|
items: fi.notes(state.items, state.selectedFolderId),
|
||||||
|
selectedNoteId: state.selectedNoteId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = function(dispatch) {
|
||||||
|
return { }
|
||||||
|
}
|
||||||
|
|
||||||
|
const NoteList = connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(NoteListComponent)
|
||||||
|
|
||||||
|
export default NoteList
|
31
spa_client/src/js/components/root-folder-list.jsx
Executable file
31
spa_client/src/js/components/root-folder-list.jsx
Executable file
@ -0,0 +1,31 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import FolderList from './folder-list.jsx';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import * as fi from 'models/folder-item.jsx';
|
||||||
|
|
||||||
|
class RootFolderListComponent extends React.Component {
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return <FolderList expandedFolderIds={this.props.expandedFolderIds} level="0" parentId="0" items={this.props.items} selectedFolderId={this.props.selectedFolderId} />
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapStateToProps = function(state) {
|
||||||
|
return {
|
||||||
|
items: fi.folders(state.items),
|
||||||
|
selectedFolderId: state.selectedFolderId,
|
||||||
|
expandedFolderIds: state.expandedFolderIds,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = function(dispatch) {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const RootFolderList = connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(RootFolderListComponent)
|
||||||
|
|
||||||
|
export default RootFolderList
|
42
spa_client/src/js/models/folder-item.jsx
Executable file
42
spa_client/src/js/models/folder-item.jsx
Executable file
@ -0,0 +1,42 @@
|
|||||||
|
export function rootItems(items) {
|
||||||
|
var output = [];
|
||||||
|
items.forEach((item, index) => {
|
||||||
|
if (!item.parent_id) return;
|
||||||
|
output.push(item);
|
||||||
|
});
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function folders(items) {
|
||||||
|
var output = [];
|
||||||
|
items.forEach((item, index) => {
|
||||||
|
if (item.type != 1) return;
|
||||||
|
output.push(item);
|
||||||
|
});
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function notes(items, folderId) {
|
||||||
|
var output = [];
|
||||||
|
items.forEach((item, index) => {
|
||||||
|
if (item.type != 2) return;
|
||||||
|
if (item.parent_id != folderId) return;
|
||||||
|
output.push(item);
|
||||||
|
});
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function byId(items, id) {
|
||||||
|
for (let i = 0; i < items.length; i++) {
|
||||||
|
if (items[i].id == id) return items[i];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function children(items, id) {
|
||||||
|
var output = [];
|
||||||
|
for (let i = 0; i < items.length; i++) {
|
||||||
|
if (items[i].parent_id == id) output.push(items[i]);
|
||||||
|
}
|
||||||
|
return output;
|
||||||
|
}
|
32
spa_client/src/js/reducer.jsx
Executable file
32
spa_client/src/js/reducer.jsx
Executable file
@ -0,0 +1,32 @@
|
|||||||
|
import deepcopy from 'deepcopy';
|
||||||
|
|
||||||
|
export function reducer(state, action) {
|
||||||
|
switch (action.type) {
|
||||||
|
|
||||||
|
case 'SELECT_FOLDER':
|
||||||
|
|
||||||
|
var state = deepcopy(state);
|
||||||
|
state.selectedFolderId = action.id;
|
||||||
|
return state;
|
||||||
|
|
||||||
|
case 'SELECT_NOTE':
|
||||||
|
|
||||||
|
var state = deepcopy(state);
|
||||||
|
state.selectedNoteId = action.id;
|
||||||
|
return state;
|
||||||
|
|
||||||
|
case 'TOGGLE_FOLDER':
|
||||||
|
|
||||||
|
var state = deepcopy(state);
|
||||||
|
var idx = state.expandedFolderIds.indexOf(action.id);
|
||||||
|
if (idx < 0) {
|
||||||
|
state.expandedFolderIds.push(action.id);
|
||||||
|
} else {
|
||||||
|
state.expandedFolderIds.splice(idx, 1);
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return state;
|
||||||
|
}
|
128
spa_client/src/js/simple-example.js
Executable file
128
spa_client/src/js/simple-example.js
Executable file
@ -0,0 +1,128 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom';
|
||||||
|
import { render } from 'react-dom'
|
||||||
|
import { connect } from 'react-redux'
|
||||||
|
import { createStore } from 'redux';
|
||||||
|
import immutable from 'object-path-immutable';
|
||||||
|
import { Provider } from 'react-redux'
|
||||||
|
import deepcopy from 'deepcopy';
|
||||||
|
|
||||||
|
// This is the default state of the application. Each
|
||||||
|
// application has only one state, which is an object
|
||||||
|
// with one or more properties.
|
||||||
|
|
||||||
|
let defaultState = {
|
||||||
|
'myButtonLabel': 'click'
|
||||||
|
}
|
||||||
|
|
||||||
|
// The reducer is what processes the actions of the application, such as
|
||||||
|
// button clicks, text changes, etc. It takes a state and an action as
|
||||||
|
// input and must return a state. Important: the state must not be modified
|
||||||
|
// directly. Create a copy first (see `deepcopy`), modify it and return it.
|
||||||
|
|
||||||
|
function reducer(state, action) {
|
||||||
|
switch (action.type) {
|
||||||
|
|
||||||
|
case 'SET_BUTTON_NAME':
|
||||||
|
|
||||||
|
var state = deepcopy(state);
|
||||||
|
state.myButtonLabel = action.name;
|
||||||
|
return state;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The store is what essentially links the reducer to the state.
|
||||||
|
|
||||||
|
let store = createStore(reducer, defaultState)
|
||||||
|
|
||||||
|
// Create the button and input components. Those are regular React components.
|
||||||
|
|
||||||
|
class MyButton extends React.Component {
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return <button onClick={this.props.onClick}>{this.props.label}</button>
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class MyInput extends React.Component {
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return <input onKeyPress={this.props.onKeyPress} type="text" />
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the connected components. A connected component (often called "container component")
|
||||||
|
// is a react component that has been connected to the application state. The connection
|
||||||
|
// happens by mapping the state properties to the component properties, and by mapping the
|
||||||
|
// dispatches to the component handlers. This allows better separating the view (React
|
||||||
|
// component) from the model/controller (state and actions).
|
||||||
|
|
||||||
|
const mapStateToButtonProps = function(state) {
|
||||||
|
return { label: state.myButtonLabel };
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToButtonProps = function(dispatch) {
|
||||||
|
return {
|
||||||
|
onClick: function() {
|
||||||
|
alert('click');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const MyConnectedButton = connect(
|
||||||
|
mapStateToButtonProps,
|
||||||
|
mapDispatchToButtonProps
|
||||||
|
)(MyButton)
|
||||||
|
|
||||||
|
const mapStateToInputProps = function(state) {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToInputProps = function(dispatch) {
|
||||||
|
return {
|
||||||
|
onKeyPress(e) {
|
||||||
|
if (e.key == 'Enter') {
|
||||||
|
dispatch({
|
||||||
|
type: 'SET_BUTTON_NAME',
|
||||||
|
name: e.target.value
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const MyConnectionInput = connect(
|
||||||
|
mapStateToInputProps,
|
||||||
|
mapDispatchToInputProps
|
||||||
|
)(MyInput)
|
||||||
|
|
||||||
|
// Create the application. Note that we display the Connected components,
|
||||||
|
// which in turn include the Rect components.
|
||||||
|
|
||||||
|
class App extends React.Component {
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<MyConnectedButton />
|
||||||
|
<MyConnectionInput />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render the application via the <Provider> tag. This is a special React-Redux
|
||||||
|
// component that "magically" links the store to the application and components.
|
||||||
|
|
||||||
|
render(
|
||||||
|
<Provider store={store}>
|
||||||
|
<App />
|
||||||
|
</Provider>,
|
||||||
|
document.getElementById('container')
|
||||||
|
)
|
38
spa_client/webpack.config.js
Executable file
38
spa_client/webpack.config.js
Executable file
@ -0,0 +1,38 @@
|
|||||||
|
var path = require('path');
|
||||||
|
var webpack = require('webpack');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
quiet: true,
|
||||||
|
entry: [
|
||||||
|
'./src/js/app.jsx'
|
||||||
|
],
|
||||||
|
output: {
|
||||||
|
path: __dirname + '/dist/js/',
|
||||||
|
filename: 'app.js',
|
||||||
|
publicPath: '/js/'
|
||||||
|
},
|
||||||
|
module: {
|
||||||
|
loaders: [{
|
||||||
|
test: /\.(jsx|js)$/,
|
||||||
|
loaders: ['babel'],
|
||||||
|
include: path.join(__dirname, './src/js/')
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
components: path.resolve(__dirname, './src/js/components'),
|
||||||
|
models: path.resolve(__dirname, './src/js/models'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
// new webpack.optimize.UglifyJsPlugin({
|
||||||
|
// minimize: true,
|
||||||
|
// compress: {
|
||||||
|
// warnings: false
|
||||||
|
// }
|
||||||
|
// }),
|
||||||
|
new webpack.DefinePlugin({
|
||||||
|
'process.env.NODE_ENV': '"development"'
|
||||||
|
})
|
||||||
|
]
|
||||||
|
};
|
32
spa_client/webpack.dev.config.js
Executable file
32
spa_client/webpack.dev.config.js
Executable file
@ -0,0 +1,32 @@
|
|||||||
|
// NOT USED
|
||||||
|
|
||||||
|
var path = require('path');
|
||||||
|
var webpack = require('webpack');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
entry: [
|
||||||
|
'./src/js/app'
|
||||||
|
],
|
||||||
|
target: 'web',
|
||||||
|
devtool: 'eval-source-map',
|
||||||
|
output: {
|
||||||
|
path: __dirname + '/src/js/',
|
||||||
|
filename: 'app.min.js',
|
||||||
|
publicPath: '/js/'
|
||||||
|
},
|
||||||
|
module: {
|
||||||
|
loaders: [{
|
||||||
|
test: /\.js$/,
|
||||||
|
loaders: ['babel'],
|
||||||
|
include: path.join(__dirname, './src/js/')
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
new webpack.ProvidePlugin({
|
||||||
|
'Promise': 'es6-promise'
|
||||||
|
}),
|
||||||
|
new webpack.DefinePlugin({
|
||||||
|
'process.env.NODE_ENV': '"development"'
|
||||||
|
})
|
||||||
|
]
|
||||||
|
};
|
9
src/AppBundle/ApiSerializer.php
Executable file
9
src/AppBundle/ApiSerializer.php
Executable file
@ -0,0 +1,9 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace AppBundle;
|
||||||
|
|
||||||
|
class ApiSerializer {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
84
src/AppBundle/Command/BuildMimeTypeArrayCommand.php
Executable file
84
src/AppBundle/Command/BuildMimeTypeArrayCommand.php
Executable file
@ -0,0 +1,84 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace AppBundle\Command;
|
||||||
|
|
||||||
|
use Symfony\Component\Console\Command\Command;
|
||||||
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
|
||||||
|
use AppBundle\Paths;
|
||||||
|
|
||||||
|
class BuildMimeTypeArrayCommand extends ContainerAwareCommand {
|
||||||
|
|
||||||
|
protected function configure() {
|
||||||
|
$this->setName('app:build-mime-type-array');
|
||||||
|
$this->setDescription('Build MIME type PHP array from Apache mime.types file.');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function execute(InputInterface $input, OutputInterface $output) {
|
||||||
|
$paths = $this->getContainer()->get('app.paths');
|
||||||
|
|
||||||
|
$sourcePath = $paths->dataDir() . '/mime.types';
|
||||||
|
$targetPath = $paths->dataDir() . '/mime_types.php';
|
||||||
|
$sqlitePath = $paths->dataDir() . '/mime_types.sqlite.sql';
|
||||||
|
|
||||||
|
if (file_exists($targetPath)) {
|
||||||
|
// TODO: (or not)
|
||||||
|
// To update the existing PHP file, it needs to be loaded first and the IDs need to remain in place
|
||||||
|
// since they might be use in the database, etc., then any new type can be added.
|
||||||
|
// This is easy to implement, but most likely YAGNI.
|
||||||
|
throw new \Exception(sprintf('MimeType PHP file already exists at %s and the code to update it as not been implemented.', $targetPath));
|
||||||
|
}
|
||||||
|
|
||||||
|
$lines = file_get_contents($sourcePath);
|
||||||
|
if ($lines === false) throw new \Exception('Cannot read ' . $sourcePath);
|
||||||
|
$lines = explode("\n", $lines);
|
||||||
|
|
||||||
|
$mimeTypes = array();
|
||||||
|
$id = 1;
|
||||||
|
foreach ($lines as $line) {
|
||||||
|
$line = trim($line);
|
||||||
|
if (!$line) continue;
|
||||||
|
if ($line[0] == '#') continue;
|
||||||
|
$tokens = explode("\t", $line);
|
||||||
|
if (count($tokens) < 2) continue;
|
||||||
|
|
||||||
|
$mimeType = trim($tokens[0]);
|
||||||
|
if (!$mimeType) continue; // Shouldn't happen
|
||||||
|
$extensions = explode(' ', $tokens[count($tokens) - 1]);
|
||||||
|
|
||||||
|
$mimeTypes[$id] = array(
|
||||||
|
'id' => $id,
|
||||||
|
't' => $mimeType,
|
||||||
|
'e' => $extensions,
|
||||||
|
);
|
||||||
|
|
||||||
|
$id++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// CREATE TABLE mimetypes (
|
||||||
|
// id INT,
|
||||||
|
// mime TEXT
|
||||||
|
// );
|
||||||
|
|
||||||
|
// CREATE TABLE mimetype_extensions (
|
||||||
|
// id INTEGER PRIMARY KEY,
|
||||||
|
// mimetype_id,
|
||||||
|
// extension TEXT
|
||||||
|
// );
|
||||||
|
|
||||||
|
// foreach ($mimeTypes[$id]
|
||||||
|
|
||||||
|
$content = var_export($mimeTypes, true);
|
||||||
|
$content = str_replace(' ', '', $content);
|
||||||
|
$content = str_replace("\n", '', $content);
|
||||||
|
$content = str_replace(',)', ')', $content);
|
||||||
|
$content = '<?php return ' . $content . ';';
|
||||||
|
|
||||||
|
$ok = file_put_contents($targetPath, $content);
|
||||||
|
if ($ok === false) throw new \Exception('Could not write to ' . $targetPath);
|
||||||
|
|
||||||
|
$output->writeln(sprintf('File created at "%s"', $targetPath));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -103,6 +103,11 @@ abstract class ApiController extends Controller {
|
|||||||
return $this->user;
|
return $this->user;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function userId() {
|
||||||
|
$u = $this->user();
|
||||||
|
return $u ? $u->id : 0;
|
||||||
|
}
|
||||||
|
|
||||||
protected function aclCheck($resource) {
|
protected function aclCheck($resource) {
|
||||||
if (!is_array($resource)) $resource = array($resource);
|
if (!is_array($resource)) $resource = array($resource);
|
||||||
$user = $this->user();
|
$user = $this->user();
|
||||||
|
61
src/AppBundle/Controller/FilesController.php
Executable file
61
src/AppBundle/Controller/FilesController.php
Executable file
@ -0,0 +1,61 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace AppBundle\Controller;
|
||||||
|
|
||||||
|
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use AppBundle\Controller\ApiController;
|
||||||
|
use AppBundle\Model\BaseModel;
|
||||||
|
use AppBundle\Model\File;
|
||||||
|
use AppBundle\Exception\ValidationException;
|
||||||
|
|
||||||
|
class FilesController extends ApiController {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Route("/files")
|
||||||
|
*/
|
||||||
|
public function allAction(Request $request) {
|
||||||
|
if ($request->isMethod('POST')) {
|
||||||
|
if (!isset($_FILES['file'])) throw new ValidationException('Missing "file" parameter');
|
||||||
|
|
||||||
|
$file = new File();
|
||||||
|
$file->moveUploadedFile($_FILES['file']);
|
||||||
|
$file->owner_id = $this->userId();
|
||||||
|
$file->save();
|
||||||
|
|
||||||
|
return static::successResponse($file);
|
||||||
|
}
|
||||||
|
|
||||||
|
return static::errorResponse('Invalid method');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Route("/files/{id}")
|
||||||
|
*/
|
||||||
|
public function oneAction($id, Request $request) {
|
||||||
|
$file = File::find(File::unhex($id));
|
||||||
|
if (!$file) return static::errorResponse('Not found', 0, 404);
|
||||||
|
|
||||||
|
if ($request->isMethod('GET')) {
|
||||||
|
return static::successResponse($file);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->isMethod('PATCH')) {
|
||||||
|
$data = $this->patchParameters();
|
||||||
|
foreach ($data as $n => $v) {
|
||||||
|
$file->{$n} = $v;
|
||||||
|
}
|
||||||
|
$file->save();
|
||||||
|
return static::successResponse($file);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->isMethod('DELETE')) {
|
||||||
|
$file->delete();
|
||||||
|
return static::successResponse();
|
||||||
|
}
|
||||||
|
|
||||||
|
return static::errorResponse('Invalid method');
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -9,7 +9,7 @@ use AppBundle\Controller\ApiController;
|
|||||||
use AppBundle\Model\User;
|
use AppBundle\Model\User;
|
||||||
use AppBundle\Model\Session;
|
use AppBundle\Model\Session;
|
||||||
use AppBundle\Model\Change;
|
use AppBundle\Model\Change;
|
||||||
use AppBundle\Model\FolderItem;
|
use AppBundle\Model\BaseItem;
|
||||||
use AppBundle\Exception\ValidationException;
|
use AppBundle\Exception\ValidationException;
|
||||||
|
|
||||||
|
|
||||||
@ -127,45 +127,45 @@ class UsersController extends ApiController {
|
|||||||
// var_dump($diff2);
|
// var_dump($diff2);
|
||||||
|
|
||||||
// $change = new Change();
|
// $change = new Change();
|
||||||
// $change->user_id = FolderItem::unhex('204705F2E2E698036034FDC709840B80');
|
// $change->user_id = BaseItem::unhex('204705F2E2E698036034FDC709840B80');
|
||||||
// $change->client_id = FolderItem::unhex('11111111111111111111111111111111');
|
// $change->client_id = BaseItem::unhex('11111111111111111111111111111111');
|
||||||
// $change->item_type = FolderItem::enumId('type', 'note');
|
// $change->item_type = BaseItem::enumId('type', 'note');
|
||||||
// $change->item_field = FolderItem::enumId('field', 'title');
|
// $change->item_field = BaseItem::enumId('field', 'title');
|
||||||
// $change->item_id = FolderItem::unhex('DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD');
|
// $change->item_id = BaseItem::unhex('DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD');
|
||||||
// $change->delta = 'salut ca va';
|
// $change->delta = 'salut ca va';
|
||||||
// $change->save();
|
// $change->save();
|
||||||
|
|
||||||
|
|
||||||
// $change = new Change();
|
// $change = new Change();
|
||||||
// $change->user_id = FolderItem::unhex('204705F2E2E698036034FDC709840B80');
|
// $change->user_id = BaseItem::unhex('204705F2E2E698036034FDC709840B80');
|
||||||
// $change->client_id = FolderItem::unhex('11111111111111111111111111111111');
|
// $change->client_id = BaseItem::unhex('11111111111111111111111111111111');
|
||||||
// $change->item_type = FolderItem::enumId('type', 'note');
|
// $change->item_type = BaseItem::enumId('type', 'note');
|
||||||
// $change->item_field = FolderItem::enumId('field', 'title');
|
// $change->item_field = BaseItem::enumId('field', 'title');
|
||||||
// $change->item_id = FolderItem::unhex('DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD');
|
// $change->item_id = BaseItem::unhex('DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD');
|
||||||
// $change->createDelta('salut, ça va ? oui très bien');
|
// $change->createDelta('salut, ça va ? oui très bien');
|
||||||
// $change->save();
|
// $change->save();
|
||||||
|
|
||||||
// $change = new Change();
|
// $change = new Change();
|
||||||
// $change->user_id = FolderItem::unhex('204705F2E2E698036034FDC709840B80');
|
// $change->user_id = BaseItem::unhex('204705F2E2E698036034FDC709840B80');
|
||||||
// $change->client_id = FolderItem::unhex('11111111111111111111111111111111');
|
// $change->client_id = BaseItem::unhex('11111111111111111111111111111111');
|
||||||
// $change->item_type = FolderItem::enumId('type', 'note');
|
// $change->item_type = BaseItem::enumId('type', 'note');
|
||||||
// $change->item_field = FolderItem::enumId('field', 'title');
|
// $change->item_field = BaseItem::enumId('field', 'title');
|
||||||
// $change->item_id = FolderItem::unhex('DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD');
|
// $change->item_id = BaseItem::unhex('DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD');
|
||||||
// $change->createDelta('salut - oui très bien');
|
// $change->createDelta('salut - oui très bien');
|
||||||
// $change->save();
|
// $change->save();
|
||||||
|
|
||||||
// $change = new Change();
|
// $change = new Change();
|
||||||
// $change->user_id = FolderItem::unhex('204705F2E2E698036034FDC709840B80');
|
// $change->user_id = BaseItem::unhex('204705F2E2E698036034FDC709840B80');
|
||||||
// $change->client_id = FolderItem::unhex('11111111111111111111111111111111');
|
// $change->client_id = BaseItem::unhex('11111111111111111111111111111111');
|
||||||
// $change->item_type = FolderItem::enumId('type', 'note');
|
// $change->item_type = BaseItem::enumId('type', 'note');
|
||||||
// $change->item_field = FolderItem::enumId('field', 'title');
|
// $change->item_field = BaseItem::enumId('field', 'title');
|
||||||
// $change->item_id = FolderItem::unhex('DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD');
|
// $change->item_id = BaseItem::unhex('DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD');
|
||||||
// $change->createDelta('salut, ça va ? oui bien');
|
// $change->createDelta('salut, ça va ? oui bien');
|
||||||
// $change->save();
|
// $change->save();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
$d = Change::fullFieldText(FolderItem::unhex('DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD'), FolderItem::enumId('field', 'title'));
|
$d = Change::fullFieldText(BaseItem::unhex('DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD'), BaseItem::enumId('field', 'title'));
|
||||||
var_dump($d);die();
|
var_dump($d);die();
|
||||||
|
|
||||||
|
|
||||||
|
@ -6,21 +6,26 @@ class Eloquent {
|
|||||||
|
|
||||||
private $capsule_ = null;
|
private $capsule_ = null;
|
||||||
|
|
||||||
public function __construct() {
|
public function __construct($mimeTypes, $paths) {
|
||||||
$this->capsule_ = new \Illuminate\Database\Capsule\Manager();
|
$this->capsule_ = new \Illuminate\Database\Capsule\Manager();
|
||||||
|
|
||||||
$this->capsule_->addConnection([
|
$this->capsule_->addConnection([
|
||||||
'driver' => 'mysql',
|
'driver' => 'mysql',
|
||||||
'host' => '127.0.0.1',
|
'host' => 'localhost',
|
||||||
'database' => 'notes',
|
'database' => 'notes',
|
||||||
'username' => 'root',
|
'username' => 'root',
|
||||||
'password' => '',
|
'password' => 'pass',
|
||||||
'charset' => 'utf8',
|
'charset' => 'utf8',
|
||||||
'collation' => 'utf8_unicode_ci',
|
'collation' => 'utf8_unicode_ci',
|
||||||
'prefix' => '',
|
'prefix' => '',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->capsule_->bootEloquent();
|
$this->capsule_->bootEloquent();
|
||||||
|
|
||||||
|
// In order to keep things lightweight, the models aren't part of Symfony dependency
|
||||||
|
// injection framework, so any service required by a model needs to be injected here.
|
||||||
|
Model\File::$mimeTypes = $mimeTypes;
|
||||||
|
Model\File::$paths = $paths;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function connection() {
|
public function connection() {
|
||||||
|
84
src/AppBundle/MimeTypes.php
Executable file
84
src/AppBundle/MimeTypes.php
Executable file
@ -0,0 +1,84 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace AppBundle;
|
||||||
|
|
||||||
|
class MimeTypes {
|
||||||
|
|
||||||
|
private $mimeTypes_ = null;
|
||||||
|
private $paths_ = null;
|
||||||
|
private $defaultMimeType_ = 'application/octet-stream';
|
||||||
|
private $defaultMimeTypeId_ = null;
|
||||||
|
|
||||||
|
public function __construct($paths) {
|
||||||
|
$this->paths_ = $paths;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the mime type from the file extension, if any, or from
|
||||||
|
// the file content. The second check will be skipped if the
|
||||||
|
// file doesn't exists.
|
||||||
|
public function idFromPath($filePath) {
|
||||||
|
$ext = pathinfo($filePath, PATHINFO_EXTENSION);
|
||||||
|
$r = $this->extensionToMimeTypeId($ext);
|
||||||
|
if ($r !== $this->defaultMimeTypeId()) return $r;
|
||||||
|
|
||||||
|
// Else try to get the mime type from the binary content
|
||||||
|
if (!file_exists($filePath)) return $this->defaultMimeTypeId();
|
||||||
|
|
||||||
|
$finfo = finfo_open(FILEINFO_MIME_TYPE);
|
||||||
|
$r = finfo_file($finfo, $filePath);
|
||||||
|
finfo_close($finfo);
|
||||||
|
return $r === false ? $this->defaultMimeTypeId() : $this->stringToId($r);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function loadMimeTypes() {
|
||||||
|
if ($this->mimeTypes_) return;
|
||||||
|
$path = $this->paths_->dataDir() . '/mime_types.php';
|
||||||
|
if (!file_exists($path)) throw new \Exception(sprintf('File not found: "%s"', $path));
|
||||||
|
$this->mimeTypes_ = require $path;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function defaultMimeTypeId() {
|
||||||
|
if ($this->defaultMimeTypeId_) return $this->defaultMimeTypeId_;
|
||||||
|
$this->loadMimeTypes();
|
||||||
|
$this->defaultMimeTypeId_ = $this->stringToId($this->defaultMimeType_);
|
||||||
|
return $this->defaultMimeTypeId_;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function defaultMimeType() {
|
||||||
|
return $this->defaultMimeType_;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function idToString($id) {
|
||||||
|
$this->loadMimeTypes();
|
||||||
|
$id = (int)$id;
|
||||||
|
if (!isset($this->mimeTypes_[$id])) throw new \Exception(sprintf('Invalid MIME type ID: %s', $id));
|
||||||
|
return $this->mimeTypes_[$id]['t'];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stringToId($mimeType) {
|
||||||
|
$this->loadMimeTypes();
|
||||||
|
$mimeType = strtolower(trim($mimeType));
|
||||||
|
if (!$mimeType) return $this->defaultMimeTypeId();
|
||||||
|
|
||||||
|
foreach ($this->mimeTypes_ as $id => $o) {
|
||||||
|
if ($o['t'] == $mimeType) return $id;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->defaultMimeTypeId();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function extensionToMimeTypeId($ext) {
|
||||||
|
$this->loadMimeTypes();
|
||||||
|
$ext = strtolower(trim($ext));
|
||||||
|
if (!$ext) return $this->defaultMimeTypeId();
|
||||||
|
|
||||||
|
foreach ($this->mimeTypes_ as $id => $o) {
|
||||||
|
foreach ($o['e'] as $e) {
|
||||||
|
if ($ext == $e) return $id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->defaultMimeTypeId();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
45
src/AppBundle/Model/BaseItem.php
Executable file
45
src/AppBundle/Model/BaseItem.php
Executable file
@ -0,0 +1,45 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace AppBundle\Model;
|
||||||
|
|
||||||
|
class BaseItem extends BaseModel {
|
||||||
|
|
||||||
|
public $useUuid = true;
|
||||||
|
public $incrementing = false;
|
||||||
|
|
||||||
|
static protected $enums = array(
|
||||||
|
'type' => array('folder', 'note', 'todo', 'tag'),
|
||||||
|
);
|
||||||
|
|
||||||
|
public function itemTypeId() {
|
||||||
|
$typeName = null;
|
||||||
|
switch (get_called_class()) {
|
||||||
|
case 'AppBundle\Model\Folder':
|
||||||
|
$typeName = 'folder';
|
||||||
|
break;
|
||||||
|
case 'AppBundle\Model\Note':
|
||||||
|
$typeName = 'note';
|
||||||
|
break;
|
||||||
|
case 'AppBundle\Model\Tag':
|
||||||
|
$typeName = 'tag';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$typeName) throw new \Exception('Unknown item class: ' . get_called_class());
|
||||||
|
|
||||||
|
return self::enumId('type', $typeName);
|
||||||
|
}
|
||||||
|
|
||||||
|
static public function byId($itemTypeId, $itemId) {
|
||||||
|
if ($itemTypeId == BaseItem::enumId('type', 'folder')) {
|
||||||
|
return Folder::where('id', '=', $itemId)->first();
|
||||||
|
} else if ($itemTypeId == BaseItem::enumId('type', 'note')) {
|
||||||
|
return Note::where('id', '=', $itemId)->first();
|
||||||
|
} else if ($itemTypeId == BaseItem::enumId('type', 'tag')) {
|
||||||
|
return Tag::where('id', '=', $itemId)->first();
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new \Exception('Unsupported item type: ' . $itemTypeId);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -157,7 +157,7 @@ class BaseModel extends \Illuminate\Database\Eloquent\Model {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isset($output['item_type'])) {
|
if (isset($output['item_type'])) {
|
||||||
$output['item_type'] = FolderItem::enumName('type', $output['item_type'], true);
|
$output['item_type'] = BaseItem::enumName('type', $output['item_type'], true);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isset($output['item_field'])) {
|
if (isset($output['item_field'])) {
|
||||||
@ -311,8 +311,12 @@ class BaseModel extends \Illuminate\Database\Eloquent\Model {
|
|||||||
$this->isNew = $v;
|
$this->isNew = $v;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function isNew() {
|
||||||
|
return !$this->id || $this->isNew === true;
|
||||||
|
}
|
||||||
|
|
||||||
public function save(Array $options = array()) {
|
public function save(Array $options = array()) {
|
||||||
$isNew = !$this->id || $this->isNew === true;
|
$isNew = $this->isNew();
|
||||||
|
|
||||||
if ($this->useUuid && $isNew && !$this->id) $this->id = self::createId();
|
if ($this->useUuid && $isNew && !$this->id) $this->id = self::createId();
|
||||||
$this->updated_time = time(); // TODO: maybe only update if one of the fields, or if some of versioned data has changed
|
$this->updated_time = time(); // TODO: maybe only update if one of the fields, or if some of versioned data has changed
|
||||||
@ -320,6 +324,8 @@ class BaseModel extends \Illuminate\Database\Eloquent\Model {
|
|||||||
|
|
||||||
parent::save($options);
|
parent::save($options);
|
||||||
|
|
||||||
|
$this->isNew = null;
|
||||||
|
|
||||||
if (count($this->versionedFields)) {
|
if (count($this->versionedFields)) {
|
||||||
$this->recordChanges($isNew ? 'create' : 'update', $this->changedVersionedFieldValues);
|
$this->recordChanges($isNew ? 'create' : 'update', $this->changedVersionedFieldValues);
|
||||||
}
|
}
|
||||||
@ -364,7 +370,7 @@ class BaseModel extends \Illuminate\Database\Eloquent\Model {
|
|||||||
$change = new Change();
|
$change = new Change();
|
||||||
$change->user_id = $this->owner_id;
|
$change->user_id = $this->owner_id;
|
||||||
$change->client_id = static::clientId();
|
$change->client_id = static::clientId();
|
||||||
$change->item_type = FolderItem::enumId('type', $this->classItemTypeName());
|
$change->item_type = BaseItem::enumId('type', $this->classItemTypeName());
|
||||||
$change->type = Change::enumId('type', $type);
|
$change->type = Change::enumId('type', $type);
|
||||||
$change->item_id = $this->id;
|
$change->item_id = $this->id;
|
||||||
return $change;
|
return $change;
|
||||||
|
52
src/AppBundle/Model/File.php
Executable file
52
src/AppBundle/Model/File.php
Executable file
@ -0,0 +1,52 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace AppBundle\Model;
|
||||||
|
|
||||||
|
class File extends BaseModel {
|
||||||
|
|
||||||
|
static public $mimeTypes = null;
|
||||||
|
static public $paths = null;
|
||||||
|
|
||||||
|
public $useUuid = true;
|
||||||
|
public $incrementing = false;
|
||||||
|
|
||||||
|
public function toPublicArray() {
|
||||||
|
$output = parent::toPublicArray();
|
||||||
|
$output['mime_type'] = self::$mimeTypes->idToString($this->mime_type);
|
||||||
|
return $output;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function delete() {
|
||||||
|
@unlink($this->path());
|
||||||
|
parent::delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function path() {
|
||||||
|
return self::$paths->uploadsDir() . '/' . BaseModel::hex($this->id);
|
||||||
|
}
|
||||||
|
|
||||||
|
static public function pathForId($id) {
|
||||||
|
return self::$paths->uploadsDir() . '/' . $id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function moveUploadedFile($file) {
|
||||||
|
if (isset($file['error']) && $file['error']) throw new \Exception('Cannot upload file: ' . $file['error']);
|
||||||
|
|
||||||
|
$filePath = $file['tmp_name'];
|
||||||
|
$originalName = $file['name'];
|
||||||
|
$id = BaseModel::createId();
|
||||||
|
|
||||||
|
$targetPath = self::$paths->uploadsDir() . '/' . BaseModel::hex($id);
|
||||||
|
if (file_exists($targetPath)) throw new \Exception('Hash collision'); // Shouldn't happen
|
||||||
|
|
||||||
|
if (!@move_uploaded_file($filePath, $targetPath)) throw new \Exception('Unknown error - file could not be uploaded.');
|
||||||
|
|
||||||
|
$this->id = $id;
|
||||||
|
$this->title = $originalName;
|
||||||
|
$this->original_name = $originalName;
|
||||||
|
$this->mime_type = self::$mimeTypes->idFromPath($targetPath);
|
||||||
|
|
||||||
|
$this->setIsNew(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
namespace AppBundle\Model;
|
namespace AppBundle\Model;
|
||||||
|
|
||||||
class Folder extends FolderItem {
|
class Folder extends BaseItem {
|
||||||
|
|
||||||
protected $versionedFields = array('title');
|
protected $versionedFields = array('title');
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
namespace AppBundle\Model;
|
namespace AppBundle\Model;
|
||||||
|
|
||||||
class Note extends FolderItem {
|
class Note extends BaseItem {
|
||||||
|
|
||||||
protected $versionedFields = array('title', 'body');
|
protected $versionedFields = array('title', 'body');
|
||||||
|
|
||||||
|
78
src/AppBundle/Model/Tag.php
Executable file
78
src/AppBundle/Model/Tag.php
Executable file
@ -0,0 +1,78 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace AppBundle\Model;
|
||||||
|
|
||||||
|
class Tag extends BaseItem {
|
||||||
|
|
||||||
|
// A star has the exact same behaviour as a tag, except
|
||||||
|
// it might be diplayed differently.
|
||||||
|
static private $starTag_ = null;
|
||||||
|
|
||||||
|
static public function starTag($ownerId) {
|
||||||
|
if (self::$starTag_) return self::$starTag_;
|
||||||
|
$t = Tag::where('internal', '=', 1)
|
||||||
|
->where('title', '=', 'star')
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if (!$t) {
|
||||||
|
$t = new Tag();
|
||||||
|
$t->title = 'star';
|
||||||
|
$t->internal = 1;
|
||||||
|
$t->owner_id = $ownerId;
|
||||||
|
$t->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
self::$starTag_ = $t;
|
||||||
|
return self::$starTag_;
|
||||||
|
}
|
||||||
|
|
||||||
|
static public function star($item) {
|
||||||
|
self::starTag($item->owner_id)->add($item);
|
||||||
|
}
|
||||||
|
|
||||||
|
static public function unstar($item) {
|
||||||
|
self::starTag($item->owner_id)->remove($item);
|
||||||
|
}
|
||||||
|
|
||||||
|
static public function isStarred($item) {
|
||||||
|
return self::starTag($item->owner_id)->includes($item);
|
||||||
|
}
|
||||||
|
|
||||||
|
static public function starredItems($ownerId) {
|
||||||
|
return self::starTag($ownerId)->items();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function add($item) {
|
||||||
|
if ($this->includes($item)) return;
|
||||||
|
|
||||||
|
$t = new Tagged_item();
|
||||||
|
$t->tag_id = $this->id;
|
||||||
|
$t->item_id = $item->id;
|
||||||
|
$t->item_type = $item->itemTypeId();
|
||||||
|
$t->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function includes($item) {
|
||||||
|
return !!Tagged_item::where('item_type', '=', $item->itemTypeId())
|
||||||
|
->where('item_id', '=', $item->id)
|
||||||
|
->first();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function remove($item) {
|
||||||
|
Tagged_item::where('item_type', '=', $item->itemTypeId())
|
||||||
|
->where('item_id', '=', $item->id)
|
||||||
|
->delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: retrieve items in one SQL query
|
||||||
|
public function items() {
|
||||||
|
$output = array();
|
||||||
|
$taggedItems = Tagged_item::where('tag_id', '=', $this->id)->get();
|
||||||
|
foreach ($taggedItems as $taggedItem) {
|
||||||
|
$item = BaseItem::byId($taggedItem->item_type, $taggedItem->item_id);
|
||||||
|
$output[] = $item;
|
||||||
|
}
|
||||||
|
return $output;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
7
src/AppBundle/Model/Tagged_item.php
Executable file
7
src/AppBundle/Model/Tagged_item.php
Executable file
@ -0,0 +1,7 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace AppBundle\Model;
|
||||||
|
|
||||||
|
class Tagged_item extends BaseModel {
|
||||||
|
|
||||||
|
}
|
25
src/AppBundle/Paths.php
Executable file
25
src/AppBundle/Paths.php
Executable file
@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace AppBundle;
|
||||||
|
|
||||||
|
class Paths {
|
||||||
|
|
||||||
|
private $rootDir_ = null;
|
||||||
|
|
||||||
|
public function __construct($rootDir) {
|
||||||
|
$this->rootDir_ = $rootDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rootDir() {
|
||||||
|
return $this->rootDir_;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function dataDir() {
|
||||||
|
return $this->rootDir() . '/data';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function uploadsDir() {
|
||||||
|
return $this->dataDir() . '/uploads';
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -14,10 +14,39 @@ CREATE TABLE `notes` (
|
|||||||
`completed` tinyint(1) NOT NULL default '0',
|
`completed` tinyint(1) NOT NULL default '0',
|
||||||
`created_time` int(11) NOT NULL default '0',
|
`created_time` int(11) NOT NULL default '0',
|
||||||
`updated_time` int(11) NOT NULL default '0',
|
`updated_time` int(11) NOT NULL default '0',
|
||||||
|
`latitude` DECIMAL(10, 8) NOT NULL default '0',
|
||||||
|
`longitude` DECIMAL(11, 8) NOT NULL default '0',
|
||||||
|
`altitude` DECIMAL(9, 4) NOT NULL default '0',
|
||||||
`parent_id` binary(16) NULL default NULL,
|
`parent_id` binary(16) NULL default NULL,
|
||||||
`owner_id` binary(16),
|
`owner_id` binary(16),
|
||||||
`is_encrypted` tinyint(1) NOT NULL default '0',
|
`is_encrypted` tinyint(1) NOT NULL default '0',
|
||||||
`encryption_method` int(11) NOT NULL default '0',
|
`encryption_method` int(11) NOT NULL default '0',
|
||||||
|
`order` int(11) NOT NULL default '0',
|
||||||
|
`is_todo` tinyint(1) NOT NULL default '0',
|
||||||
|
`todo_due` int(11) NOT NULL default '0',
|
||||||
|
`todo_completed` int(11) NOT NULL default '0',
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
) CHARACTER SET=utf8;
|
||||||
|
|
||||||
|
CREATE TABLE `tags` (
|
||||||
|
`id` binary(16) NOT NULL,
|
||||||
|
`title` varchar(1024) NOT NULL default '',
|
||||||
|
`owner_id` binary(16) NOT NULL,
|
||||||
|
`internal` tinyint(1) NOT NULL default '0',
|
||||||
|
`created_time` int(11) NOT NULL default '0',
|
||||||
|
`updated_time` int(11) NOT NULL default '0',
|
||||||
|
`is_encrypted` tinyint(1) NOT NULL default '0',
|
||||||
|
`encryption_method` int(11) NOT NULL default '0',
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
) CHARACTER SET=utf8;
|
||||||
|
|
||||||
|
CREATE TABLE `tagged_items` (
|
||||||
|
`id` INT(11) NOT NULL AUTO_INCREMENT,
|
||||||
|
`tag_id` binary(16) NOT NULL,
|
||||||
|
`item_id` binary(16) NOT NULL,
|
||||||
|
`item_type` int(11) NOT NULL,
|
||||||
|
`created_time` int(11) NOT NULL default '0',
|
||||||
|
`updated_time` int(11) NOT NULL default '0',
|
||||||
PRIMARY KEY (`id`)
|
PRIMARY KEY (`id`)
|
||||||
) CHARACTER SET=utf8;
|
) CHARACTER SET=utf8;
|
||||||
|
|
||||||
@ -55,3 +84,16 @@ CREATE TABLE `changes` (
|
|||||||
`previous_id` int(11) NOT NULL default '0',
|
`previous_id` int(11) NOT NULL default '0',
|
||||||
PRIMARY KEY (`id`)
|
PRIMARY KEY (`id`)
|
||||||
) CHARACTER SET=utf8;
|
) CHARACTER SET=utf8;
|
||||||
|
|
||||||
|
CREATE TABLE `files` (
|
||||||
|
`id` binary(16) NOT NULL,
|
||||||
|
`title` varchar(256) NOT NULL default '',
|
||||||
|
`mime_type` int(11) NOT NULL default '0',
|
||||||
|
`original_name` varchar(256) NOT NULL default '',
|
||||||
|
`created_time` int(11) NOT NULL default '0',
|
||||||
|
`updated_time` int(11) NOT NULL default '0',
|
||||||
|
`owner_id` binary(16) NULL default NULL,
|
||||||
|
`is_encrypted` tinyint(1) NOT NULL default '0',
|
||||||
|
`encryption_method` int(11) NOT NULL default '0',
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
) CHARACTER SET=utf8;
|
||||||
|
18
tests/AppBundle/Controller/DefaultControllerTest.php
Executable file
18
tests/AppBundle/Controller/DefaultControllerTest.php
Executable file
@ -0,0 +1,18 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\AppBundle\Controller;
|
||||||
|
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
|
||||||
|
|
||||||
|
class DefaultControllerTest extends WebTestCase
|
||||||
|
{
|
||||||
|
public function testIndex()
|
||||||
|
{
|
||||||
|
$client = static::createClient();
|
||||||
|
|
||||||
|
$crawler = $client->request('GET', '/');
|
||||||
|
|
||||||
|
$this->assertEquals(200, $client->getResponse()->getStatusCode());
|
||||||
|
$this->assertContains('Welcome to Symfony', $crawler->filter('#container h1')->text());
|
||||||
|
}
|
||||||
|
}
|
25
tests/Model/BaseItemTest.php
Executable file
25
tests/Model/BaseItemTest.php
Executable file
@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
require_once dirname(dirname(__FILE__)) . '/setup.php';
|
||||||
|
|
||||||
|
use AppBundle\Model\BaseItem;
|
||||||
|
use AppBundle\Model\Note;
|
||||||
|
use AppBundle\Model\Folder;
|
||||||
|
use AppBundle\Model\Tag;
|
||||||
|
|
||||||
|
class BaseItemTest extends BaseTestCase {
|
||||||
|
|
||||||
|
public function setUp() {
|
||||||
|
parent::setUp();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testItemType() {
|
||||||
|
$n = new Note();
|
||||||
|
$this->assertEquals(BaseItem::enumId('type', 'note'), $n->itemTypeId());
|
||||||
|
$n = new Folder();
|
||||||
|
$this->assertEquals(BaseItem::enumId('type', 'folder'), $n->itemTypeId());
|
||||||
|
$n = new Tag();
|
||||||
|
$this->assertEquals(BaseItem::enumId('type', 'tag'), $n->itemTypeId());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -3,7 +3,7 @@
|
|||||||
require_once dirname(dirname(__FILE__)) . '/setup.php';
|
require_once dirname(dirname(__FILE__)) . '/setup.php';
|
||||||
|
|
||||||
use AppBundle\Model\BaseModel;
|
use AppBundle\Model\BaseModel;
|
||||||
use AppBundle\Model\FolderItem;
|
use AppBundle\Model\BaseItem;
|
||||||
use AppBundle\Model\Note;
|
use AppBundle\Model\Note;
|
||||||
use AppBundle\Model\Change;
|
use AppBundle\Model\Change;
|
||||||
|
|
||||||
@ -24,7 +24,7 @@ class ChangeTest extends BaseTestCase {
|
|||||||
$change = new Change();
|
$change = new Change();
|
||||||
$change->user_id = $this->user()->id;
|
$change->user_id = $this->user()->id;
|
||||||
$change->client_id = $this->clientId();
|
$change->client_id = $this->clientId();
|
||||||
$change->item_type = FolderItem::enumId('type', 'note');
|
$change->item_type = BaseItem::enumId('type', 'note');
|
||||||
$change->item_field = BaseModel::enumId('field', 'body');
|
$change->item_field = BaseModel::enumId('field', 'body');
|
||||||
$change->type = Change::enumId('type', 'create');
|
$change->type = Change::enumId('type', 'create');
|
||||||
$change->item_id = $itemId;
|
$change->item_id = $itemId;
|
||||||
@ -36,7 +36,7 @@ class ChangeTest extends BaseTestCase {
|
|||||||
$change = new Change();
|
$change = new Change();
|
||||||
$change->user_id = $this->user()->id;
|
$change->user_id = $this->user()->id;
|
||||||
$change->client_id = $this->clientId();
|
$change->client_id = $this->clientId();
|
||||||
$change->item_type = FolderItem::enumId('type', 'note');
|
$change->item_type = BaseItem::enumId('type', 'note');
|
||||||
$change->item_field = BaseModel::enumId('field', 'body');
|
$change->item_field = BaseModel::enumId('field', 'body');
|
||||||
$change->type = Change::enumId('type', 'update');
|
$change->type = Change::enumId('type', 'update');
|
||||||
$change->item_id = $itemId;
|
$change->item_id = $itemId;
|
||||||
@ -62,7 +62,7 @@ class ChangeTest extends BaseTestCase {
|
|||||||
$change = new Change();
|
$change = new Change();
|
||||||
$change->user_id = $this->user()->id;
|
$change->user_id = $this->user()->id;
|
||||||
$change->client_id = $this->clientId(1);
|
$change->client_id = $this->clientId(1);
|
||||||
$change->item_type = FolderItem::enumId('type', 'note');
|
$change->item_type = BaseItem::enumId('type', 'note');
|
||||||
$change->item_field = BaseModel::enumId('field', 'body');
|
$change->item_field = BaseModel::enumId('field', 'body');
|
||||||
$change->type = Change::enumId('type', 'create');
|
$change->type = Change::enumId('type', 'create');
|
||||||
$change->item_id = $itemId;
|
$change->item_id = $itemId;
|
||||||
@ -76,7 +76,7 @@ class ChangeTest extends BaseTestCase {
|
|||||||
$change = new Change();
|
$change = new Change();
|
||||||
$change->user_id = $this->user()->id;
|
$change->user_id = $this->user()->id;
|
||||||
$change->client_id = $this->clientId(2);
|
$change->client_id = $this->clientId(2);
|
||||||
$change->item_type = FolderItem::enumId('type', 'note');
|
$change->item_type = BaseItem::enumId('type', 'note');
|
||||||
$change->item_field = BaseModel::enumId('field', 'body');
|
$change->item_field = BaseModel::enumId('field', 'body');
|
||||||
$change->type = Change::enumId('type', 'update');
|
$change->type = Change::enumId('type', 'update');
|
||||||
$change->item_id = $itemId;
|
$change->item_id = $itemId;
|
||||||
@ -91,7 +91,7 @@ class ChangeTest extends BaseTestCase {
|
|||||||
$change = new Change();
|
$change = new Change();
|
||||||
$change->user_id = $this->user()->id;
|
$change->user_id = $this->user()->id;
|
||||||
$change->client_id = $this->clientId(1);
|
$change->client_id = $this->clientId(1);
|
||||||
$change->item_type = FolderItem::enumId('type', 'note');
|
$change->item_type = BaseItem::enumId('type', 'note');
|
||||||
$change->item_field = BaseModel::enumId('field', 'body');
|
$change->item_field = BaseModel::enumId('field', 'body');
|
||||||
$change->type = Change::enumId('type', 'update');
|
$change->type = Change::enumId('type', 'update');
|
||||||
$change->item_id = $itemId;
|
$change->item_id = $itemId;
|
||||||
|
71
tests/Model/TagTest.php
Executable file
71
tests/Model/TagTest.php
Executable file
@ -0,0 +1,71 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
require_once dirname(dirname(__FILE__)) . '/setup.php';
|
||||||
|
|
||||||
|
use AppBundle\Model\Note;
|
||||||
|
use AppBundle\Model\Tag;
|
||||||
|
|
||||||
|
class TagTest extends BaseTestCase {
|
||||||
|
|
||||||
|
public function setUp() {
|
||||||
|
parent::setUp();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testTagging() {
|
||||||
|
$note = new Note();
|
||||||
|
$note->save();
|
||||||
|
|
||||||
|
$tag = new Tag();
|
||||||
|
$tag->owner_id = $this->userId();
|
||||||
|
$tag->save();
|
||||||
|
|
||||||
|
$this->assertFalse($tag->includes($note));
|
||||||
|
|
||||||
|
$tag->add($note);
|
||||||
|
$items = $tag->items();
|
||||||
|
$this->assertEquals(1, count($items));
|
||||||
|
|
||||||
|
$tag->add($note);
|
||||||
|
$items = $tag->items();
|
||||||
|
$this->assertEquals(1, count($items));
|
||||||
|
|
||||||
|
$note2 = new Note();
|
||||||
|
$note2->save();
|
||||||
|
|
||||||
|
$tag->add($note2);
|
||||||
|
$items = $tag->items();
|
||||||
|
$this->assertEquals(2, count($items));
|
||||||
|
|
||||||
|
$this->assertEquals($note->id, $items[0]->id);
|
||||||
|
$this->assertEquals($note2->id, $items[1]->id);
|
||||||
|
$this->assertTrue($tag->includes($note));
|
||||||
|
$this->assertTrue($tag->includes($note2));
|
||||||
|
|
||||||
|
$tag->remove($note);
|
||||||
|
$items = $tag->items();
|
||||||
|
$this->assertEquals(1, count($items));
|
||||||
|
|
||||||
|
$tag->remove($note2);
|
||||||
|
$items = $tag->items();
|
||||||
|
$this->assertEquals(0, count($items));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testStarring() {
|
||||||
|
$note = new Note();
|
||||||
|
$note->owner_id = $this->userId();
|
||||||
|
$note->save();
|
||||||
|
|
||||||
|
$this->assertFalse(Tag::isStarred($note));
|
||||||
|
|
||||||
|
Tag::star($note);
|
||||||
|
$this->assertTrue(Tag::isStarred($note));
|
||||||
|
$this->assertEquals(1, count(Tag::starredItems($this->userId())));
|
||||||
|
|
||||||
|
Tag::star($note);
|
||||||
|
$this->assertEquals(1, count(Tag::starredItems($this->userId())));
|
||||||
|
|
||||||
|
Tag::unstar($note);
|
||||||
|
$this->assertFalse(Tag::isStarred($note));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user