Compare commits

...

5 Commits

Author SHA1 Message Date
jdg
6caa17a909 Moved to gitthub 2022-06-26 10:35:06 +00:00
jdg
3e2bb5c4a3 Highlight the page until it's saved 2022-06-26 10:04:15 +00:00
jdg
083572dfe3 Added events to know when a content is changed/saved 2022-06-25 10:28:39 +00:00
jdg
0875406bf8 x 2022-06-25 09:23:59 +00:00
jdg
4b53121953 Added missing files 2022-05-21 08:23:32 +00:00
12 changed files with 161 additions and 111 deletions

View File

@ -8,85 +8,3 @@ This app allow me to display the folders where I organize some documentation wit
Many things could be wrong (this is my first app for nextcloud), I would appreciate any comments/help Many things could be wrong (this is my first app for nextcloud), I would appreciate any comments/help
## Building the app
The app can be built by using the provided Makefile by running:
make
This requires the following things to be present:
* make
* which
* tar: for building the archive
* curl: used if phpunit and composer are not installed to fetch them from the web
* npm: for building and testing everything JS, only required if a package.json is placed inside the **js/** folder
The make command will install or update Composer dependencies if a composer.json is present and also **npm run build** if a package.json is present in the **js/** folder. The npm **build** script should use local paths for build systems and package managers, so people that simply want to build the app won't need to install npm libraries globally, e.g.:
**package.json**:
```json
"scripts": {
"test": "node node_modules/gulp-cli/bin/gulp.js karma",
"prebuild": "npm install && node_modules/bower/bin/bower install && node_modules/bower/bin/bower update",
"build": "node node_modules/gulp-cli/bin/gulp.js"
}
```
## Publish to App Store
First get an account for the [App Store](http://apps.nextcloud.com/) then run:
make && make appstore
The archive is located in build/artifacts/appstore and can then be uploaded to the App Store.
## Running tests
You can use the provided Makefile to run all tests by using:
make test
This will run the PHP unit and integration tests and if a package.json is present in the **js/** folder will execute **npm run test**
Of course you can also install [PHPUnit](http://phpunit.de/getting-started.html) and use the configurations directly:
phpunit -c phpunit.xml
or:
phpunit -c phpunit.integration.xml
for integration tests
// Markdown editor: https://simplemde.com/
// https://github.com/Ionaru/easy-markdown-editor
//---
https://docs.nextcloud.com/server/latest/developer_manual/app_development/tutorial.html
cd nextcloud
php -S localhost:8080
podman run --name=nextcloud --replace=true -p 8080:80 -v /absolute/path/to/apps:/var/www/html/custom_apps docker.io/nextcloud
sudo docker run --name=nextcloud -p 8080:80 -v /absolute/path/to/apps:/var/www/html/custom_apps nextcloud
Version000000Date20220302210900
//---
php ./occ migrations:execute <appId> <versionNumber>
Example: sudo -u www-data php ./occ migrations:execute mywiki 000000Date20220302210900
https://c.infdj.com/apps/files/?dir=/Documents/Manuals%20-%20Drivers/drivers/MAD&fileid=19227
https://docs.nextcloud.com/server/latest/developer_manual/digging_deeper/api.html
phpunit -c phpunit.integration.xml
https://github.com/nextcloud/nextcloud-vue

80
README2.md Normal file
View File

@ -0,0 +1,80 @@
## Building the app
The app can be built by using the provided Makefile by running:
make
This requires the following things to be present:
* make
* which
* tar: for building the archive
* curl: used if phpunit and composer are not installed to fetch them from the web
* npm: for building and testing everything JS, only required if a package.json is placed inside the **js/** folder
The make command will install or update Composer dependencies if a composer.json is present and also **npm run build** if a package.json is present in the **js/** folder. The npm **build** script should use local paths for build systems and package managers, so people that simply want to build the app won't need to install npm libraries globally, e.g.:
**package.json**:
```json
"scripts": {
"test": "node node_modules/gulp-cli/bin/gulp.js karma",
"prebuild": "npm install && node_modules/bower/bin/bower install && node_modules/bower/bin/bower update",
"build": "node node_modules/gulp-cli/bin/gulp.js"
}
```
## Publish to App Store
First get an account for the [App Store](http://apps.nextcloud.com/) then run:
make && make appstore
The archive is located in build/artifacts/appstore and can then be uploaded to the App Store.
## Running tests
You can use the provided Makefile to run all tests by using:
make test
This will run the PHP unit and integration tests and if a package.json is present in the **js/** folder will execute **npm run test**
Of course you can also install [PHPUnit](http://phpunit.de/getting-started.html) and use the configurations directly:
phpunit -c phpunit.xml
or:
phpunit -c phpunit.integration.xml
for integration tests
// Markdown editor: https://simplemde.com/
// https://github.com/Ionaru/easy-markdown-editor
//---
https://docs.nextcloud.com/server/latest/developer_manual/app_development/tutorial.html
cd nextcloud
php -S localhost:8080
podman run --name=nextcloud --replace=true -p 8080:80 -v /absolute/path/to/apps:/var/www/html/custom_apps docker.io/nextcloud
sudo docker run --name=nextcloud -p 8080:80 -v /absolute/path/to/apps:/var/www/html/custom_apps nextcloud
Version000000Date20220302210900
//---
php ./occ migrations:execute <appId> <versionNumber>
Example: sudo -u www-data php ./occ migrations:execute mywiki 000000Date20220302210900
https://c.infdj.com/apps/files/?dir=/Documents/Manuals%20-%20Drivers/drivers/MAD&fileid=19227
https://docs.nextcloud.com/server/latest/developer_manual/digging_deeper/api.html
phpunit -c phpunit.integration.xml
https://github.com/nextcloud/nextcloud-vue

View File

@ -11,9 +11,9 @@
<namespace>MyWiki</namespace> <namespace>MyWiki</namespace>
<category>office</category> <category>office</category>
<category>organization</category> <category>organization</category>
<bugs>https://git.jd.guillen.io/wiki4nextcloud/issues</bugs> <bugs>https://git.jd.guillen.io/mywiki/issues</bugs>
<dependencies> <dependencies>
<nextcloud min-version="12" max-version="24"/> <nextcloud min-version="12" max-version="25"/>
</dependencies> </dependencies>
<navigations> <navigations>
<navigation> <navigation>

View File

@ -1,3 +1,7 @@
#app-navigation:not(.vue) > ul > li > a[data-id].modified {
color:red;
}
li[data-id="wikis"] select { li[data-id="wikis"] select {
width: calc(100% - 50px); width: calc(100% - 50px);
} }
@ -5,15 +9,15 @@ li[data-id="wikis"] select {
li[class^="wikiPage-lvl-"] a::before { li[class^="wikiPage-lvl-"] a::before {
display: inline-block; display: inline-block;
} }
.wikiPage-lvl-1 a::before { .wikiPage-lvl-1>a::before {
content: "•"; content: "•";
width: 1em; width: 1em;
} }
.wikiPage-lvl-2 a::before { .wikiPage-lvl-2>a::before {
content: "• •"; content: "• •";
width: 1.5em; width: 1.5em;
} }
.wikiPage-lvl-3 a::before { .wikiPage-lvl-3>a::before {
content: "• • •"; content: "• • •";
width: 2em; width: 2em;
} }

View File

@ -33,6 +33,9 @@ class WikiContent {
}, },
element: this.textarea, element: this.textarea,
hideIcons:[], hideIcons:[],
insertTexts:{
image:['![](', '/index.php/core/preview?fileId=XXXX&x=3840&y=2160&a=true)']
},
minHeight:height+"px", minHeight:height+"px",
maxHeight:height+"px", maxHeight:height+"px",
sideBySideFullscreen: false, sideBySideFullscreen: false,
@ -44,7 +47,13 @@ class WikiContent {
this.timeout = null; this.timeout = null;
this.mde.codemirror.on("change", (instance, changeObj) => { this.mde.codemirror.on("change", (instance, changeObj) => {
if(self.loading) return;
console.log(changeObj); console.log(changeObj);
var event = new CustomEvent("myWiki::change", {detail:{ wikiId:self.wikiId,pageId:self.pageId }});
document.dispatchEvent(event);
clearTimeout(self.timeout); clearTimeout(self.timeout);
self.timeout = setTimeout(()=>self._mde_save(), this.waitSecondsToAutoSave); self.timeout = setTimeout(()=>self._mde_save(), this.waitSecondsToAutoSave);
}); });
@ -55,8 +64,10 @@ class WikiContent {
this.save(this.mde.value()); this.save(this.mde.value());
} }
_mde_set(content) { _mde_set(content) {
this.loading = true;
this.mde.clearAutosavedValue(); this.mde.clearAutosavedValue();
this.mde.value(content); this.mde.value(content);
this.loading = false;
} }
_mde_get() { _mde_get() {
return this.mde.value(); return this.mde.value();
@ -114,23 +125,27 @@ class WikiContent {
} }
save(content) { save(content) {
const self = this; const wikiId = this.wikiId;
console.info(`JDG :: Saving wiki page ${self.wikiId}-${self.pageId}`); const pageId = this.pageId;
if (self.wikiId<=0 || self.pageId<=0) {
console.info(`JDG :: Saving wiki page ${wikiId}-${pageId}`);
if (wikiId<=0 || pageId<=0) {
return; return;
} }
var baseUrl = OC.generateUrl('/apps/mywiki/wiki/'+self.wikiId); var baseUrl = OC.generateUrl('/apps/mywiki/wiki/'+wikiId);
$.ajax({ $.ajax({
url: baseUrl+'/'+self.pageId, url: baseUrl+'/'+pageId,
type: 'PUT', type: 'PUT',
contentType: 'application/json', contentType: 'application/json',
data: JSON.stringify({title:null, content:content}) data: JSON.stringify({title:null, content:content})
}).done(function (response) { }).done(function (response) {
console.info(`JDG :: WikiContent.save(${self.wikiId}, ${self.pageId})`, response); console.info(`JDG :: WikiContent.save(${wikiId}, ${pageId})`, response);
var event = new CustomEvent("myWiki::saved", {detail:{ wikiId:wikiId,pageId:pageId }});
document.dispatchEvent(event);
}).fail(function (response, code) { }).fail(function (response, code) {
OC.dialogs.alert('Error', t(appName,'Error saving wiki page({wikiId}, {pageId})',{wikiId:self.wikiId,pageId:self.pageId})); OC.dialogs.alert('Error', t(appName,'Error saving wiki page({wikiId}, {pageId})',{wikiId:wikiId,pageId:pageId}));
console.error(`JDG :: WikiContent.save(${self.wikiId}, ${self.pageId})`, response); console.error(`JDG :: WikiContent.save(${wikiId}, ${pageId})`, response);
}); });
} }
} }

View File

@ -5,13 +5,26 @@ class WikiPages {
* The container is the <ul> for the navigation panel * The container is the <ul> for the navigation panel
*/ */
constructor(container, onClickLoadPage) { constructor(container, onClickLoadPage) {
const self = this;
this.ul = container; this.ul = container;
this._onClickLoadPage = onClickLoadPage; this._onClickLoadPage = onClickLoadPage;
document.addEventListener("myWiki::change", function(e) {
console.log("myWiki::change",e.detail);
self.ul.querySelector(`li[data-page-id="${e.detail.pageId}"] a`).classList.add('modified');
});
document.addEventListener("myWiki::saved", function(e) {
console.log("myWiki::saved", e.detail);
self.ul.querySelector(`li[data-page-id="${e.detail.pageId}"] a`).classList.remove('modified');
});
} }
clear() { clear() {
this.wikiId = null; this.wikiId = null;
this.ul.querySelectorAll('[data-page-id]').forEach( x=>x.remove() ); this.ul.querySelectorAll('[data-page-id]').forEach( x=>x.remove() );
document.querySelectorAll('#app-navigation .active').forEach(e=>e.class.remove('active'))
} }
getWikiId() { getWikiId() {
@ -53,6 +66,11 @@ class WikiPages {
}); });
} }
highlightSelectedPage(pageId) {
this.ul.querySelectorAll('li[data-page-id]').forEach( x=>x.querySelector('a').classList.remove('active') );
this.ul.querySelector(`li[data-page-id="${pageId}"] a`).classList.add('active');
}
// ----------------------------------------------------------------------------------------- // -----------------------------------------------------------------------------------------
addListener(root) { addListener(root) {
const self = this; const self = this;
@ -67,9 +85,12 @@ class WikiPages {
onClickLoadPage(e) { onClickLoadPage(e) {
const li = e.target.closest("li[data-page-id]"); const li = e.target.closest("li[data-page-id]");
let pageId = li.dataset.pageId; let pageId = li.dataset.pageId;
this.highlightSelectedPage(pageId);
this._onClickLoadPage(this.wikiId, pageId); this._onClickLoadPage(this.wikiId, pageId);
} }
onClickEdit(e) { onClickEdit(e) {
const li = e.target.closest("li[data-page-id]"); const li = e.target.closest("li[data-page-id]");
li.querySelector("input").value = li.querySelector("a").innerText; li.querySelector("input").value = li.querySelector("a").innerText;
@ -167,6 +188,7 @@ class WikiPages {
} while(nextNode && nextNode.dataset.pid!=parent.dataset.pid); } while(nextNode && nextNode.dataset.pid!=parent.dataset.pid);
} }
let link2folder = OC.generateUrl('/apps/files?fileid='+pageId);
let li = document.createElement("li"); let li = document.createElement("li");
li.classList.add(`wikiPage-lvl-${lvl}`); li.classList.add(`wikiPage-lvl-${lvl}`);
li.dataset.pageId = pageId; li.dataset.pageId = pageId;
@ -190,7 +212,7 @@ class WikiPages {
<div class="app-navigation-entry-menu"> <div class="app-navigation-entry-menu">
<ul> <ul>
<li> <li>
<button data-id="openFolder" class="icon-folder">Open Folder</button> <a data-id="openFolder" class="icon-folder" href="${link2folder}">Open Folder</a>
</li> </li>
<li> <li>
<button data-id="add" class="icon-add">Add Page</button> <button data-id="add" class="icon-add">Add Page</button>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -45,12 +45,12 @@ class WikiMapper extends QBMapper {
if ($filter) { if ($filter) {
if (array_key_exists('title',$filter) ) { if (array_key_exists('title',$filter) ) {
$qb->where( $qb->andwhere(
$qb->expr()->eq('title', $qb->createNamedParameter($filter['title'])) $qb->expr()->eq('title', $qb->createNamedParameter($filter['title']))
); );
} }
if (array_key_exists('fileId',$filter) ) { if (array_key_exists('fileId',$filter) ) {
$qb->where( $qb->andwhere(
$qb->expr()->eq('file_id', $qb->createNamedParameter($filter['fileId'])) $qb->expr()->eq('file_id', $qb->createNamedParameter($filter['fileId']))
); );
} }

View File

@ -81,8 +81,7 @@ class WikiHelper {
if ( $this->wikiFolder->nodeExists($path) ) { if ( $this->wikiFolder->nodeExists($path) ) {
$this->getFileByName(self::WIKI_FILE)->putContent($data); $this->getFileByName(self::WIKI_FILE)->putContent($data);
} else { } else {
$this->wikiFolder $this->wikiFolder->newFile(self::WIKI_FILE, $data);
->newFile(self::WIKI_FILE, $data);
} }
} catch(\Exception $ex) { } catch(\Exception $ex) {
return false; return false;
@ -154,26 +153,22 @@ class WikiHelper {
public function getWikiPageContent($id): string { public function getWikiPageContent($id): string {
try { try {
$pageFolder = $this->getFolderById($id); $pageFolder = $this->getFolderById($id);
$path = $pageFolder->getInternalPath().'/'.self::WIKI_FILE_CONTENT; if ( $pageFolder->nodeExists(self::WIKI_FILE_CONTENT) ) {
if ( $this->wikiFolder->nodeExists($path) ) { return $pageFolder->get(self::WIKI_FILE_CONTENT)->getContent();
return $this->getFileByName(self::WIKI_FILE_CONTENT)->getContent();
} }
$this->wikiFolder->newFile(self::WIKI_FILE_CONTENT, '');
} catch(\Exception $ex) { } catch(\Exception $ex) {
return null; return null;
} }
return ''; return '';
} }
public function update(int $id, string $content) { public function update(int $id, string $content) {
try { try {
$pageFolder = $this->getFolderById($id); $pageFolder = $this->getFolderById($id);
$path = $pageFolder->getInternalPath().'/'.self::WIKI_FILE_CONTENT; if ( $pageFolder->nodeExists(self::WIKI_FILE_CONTENT) ) {
if ( $this->wikiFolder->nodeExists($path) ) { $pageFolder->get(self::WIKI_FILE_CONTENT)->putContent($content);
$this->getFileByName(self::WIKI_FILE_CONTENT)->putContent($content);
} else { } else {
$this->wikiFolder $pageFolder->newFile(self::WIKI_FILE_CONTENT, $content);
->newFile(self::WIKI_FILE_CONTENT, $content);
} }
} catch(\Exception $ex) { } catch(\Exception $ex) {
return false; return false;

View File

@ -45,7 +45,9 @@ class WikiPageService {
public function find(int $wikiId, int $id, string $userId) { public function find(int $wikiId, int $id, string $userId) {
try { try {
$wiki = $this->mapper->find($wikiId, $userId); $wiki = $this->mapper->find($wikiId, $userId);
$wikiPageContent = $this->wikiHelper->setFolderId($wiki->getFileId())->getWikiPageContent($id); $wikiPageContent = $this->wikiHelper
->setFolderId($wiki->getFileId())
->getWikiPageContent($id);
} catch(Exception $e) { } catch(Exception $e) {
$this->handleException($e); $this->handleException($e);
} }