Update note, model, page

This commit is contained in:
2024-07-16 20:57:38 +03:00
parent 9971714da7
commit 2f248a6370
33 changed files with 2234 additions and 21 deletions

47
src/myapp/acl/README.md Normal file
View File

@@ -0,0 +1,47 @@
Удаление неиспользуемых ACL
В файле specialistoff/acl/acl.py
```
# import json
# from pathlib import Path
...
# data = {}
# acl_file = Path(app.config['DIR_DATA'] + "/acl.json")
# if acl_file.exists():
# with open(acl_file, 'r') as fd:
# data = json.load(fd)
# if view_func.__module__ not in data:
# data[view_func.__module__] = []
# if view_func.__name__ not in data[view_func.__module__]:
# data[view_func.__module__].append(view_func.__name__)
# with open(acl_file, 'w') as fd:
# json.dump(data, fd)
```
```
import json
from pathlib import Path
from specialistoff import app, lib, models
data = {}
acl_file = Path(app.config['DIR_DATA'] + "/acl.json")
if acl_file.exists():
with open(acl_file, 'r') as fd:
data = json.load(fd)
for item in data:
resources = models.db_session.query(
models.ACLResource
).filter(
models.ACLResource.modulename == item
).filter(
~models.ACLResource.funcname.in_(data[item])
)
if resources.count() > 0:
print(resources.all())
```

View File

@@ -0,0 +1,6 @@
__author__ = 'RemiZOffAlex'
__email__ = 'remizoffalex@mail.ru'
from .acl import ACL
__all__ = ['ACL']

83
src/myapp/acl/acl.py Normal file
View File

@@ -0,0 +1,83 @@
__author__ = 'RemiZOffAlex'
__email__ = 'remizoffalex@mail.ru'
# import json
# from pathlib import Path
from flask import abort
from functools import wraps
from .. import app, lib, models
class ACL:
"""Декоратор для управления доступом
"""
def __init__(self, view_func):
# data = {}
# acl_file = Path(app.config['DIR_DATA'] + "/acl.json")
# if acl_file.exists():
# with open(acl_file, 'r') as fd:
# data = json.load(fd)
# if view_func.__module__ not in data:
# data[view_func.__module__] = []
# if view_func.__name__ not in data[view_func.__module__]:
# data[view_func.__module__].append(view_func.__name__)
# with open(acl_file, 'w') as fd:
# json.dump(data, fd)
funcname = view_func.__name__
modulename = view_func.__module__
resource = models.db_session.query(
models.ACLResource
).filter(
models.ACLResource.funcname == funcname,
models.ACLResource.modulename == modulename
).first()
if resource is None:
resource = models.ACLResource(
modulename=modulename,
funcname=funcname
)
models.db_session.add(resource)
models.db_session.commit()
self.view_func = view_func
wraps(view_func)(self)
def __call__(self, *args, **kwargs):
"""Текущий объект, на который проверяем наличие прав
"""
resource = models.db_session.query(
models.ACLResource.id
).filter(
models.ACLResource.funcname == self.view_func.__name__,
models.ACLResource.modulename == self.view_func.__module__
).first()
rule = models.db_session.query(
models.ACLUserRule.permission
).filter(
models.ACLUserRule.user_id == lib.get_user().id,
models.ACLUserRule.resource_id == resource.id
).first()
if rule is None:
roles = models.db_session.query(
models.ACLUserRole.role_id
).filter(
models.ACLUserRole.user_id == lib.get_user().id
)
rolerule = models.db_session.query(
models.ACLRoleRule
).filter(
models.ACLRoleRule.role_id.in_(roles),
models.ACLRoleRule.resource_id == resource.id
).first()
if rolerule is None:
if app.config['DEFAULT_PERMISSION'] == 'deny':
abort(403)
elif rolerule.permission == 'deny':
abort(403)
elif rule.permission == 'deny':
abort(403)
# maybe do something before the view_func call
response = self.view_func(*args, **kwargs)
# maybe do something after the view_func call
return response

View File

@@ -32,15 +32,19 @@ from .users import User # noqa F401
# Метки
from .tag import Tag # noqa F401
# Статьи
from .page.common import Page # noqa F401
from .page.favorite import PageFavorite # noqa F401
from .page.tag import PageTag # noqa F401
# Заметки
from .note.common import Note # noqa F401
from .note.favorite import NoteFavorite # noqa F401
from .note.tag import NoteTag # noqa F401
from .note.trash import NoteTrash # noqa F401
from .note.tree import NoteTree # noqa F401
# Статьи
from .page.common import Page # noqa F401
from .page.favorite import PageFavorite # noqa F401
from .page.tag import PageTag # noqa F401
from .page.trash import PageTrash # noqa F401
from .page.tree import PageTree # noqa F401
Base.metadata.create_all(engine)

View File

@@ -28,6 +28,21 @@ class Note(Base):
uselist=False
)
tags = relationship("NoteTag", primaryjoin="Note.id==NoteTag.note_id")
# Родитель
parents = relationship(
"NoteTree",
primaryjoin="Note.id == NoteTree.child_id",
)
# Дочерние узлы
nodes = relationship(
"NoteTree",
primaryjoin="Note.id == NoteTree.parent_id",
)
trash = relationship(
"NoteTrash",
primaryjoin="Note.id == NoteTrash.note_id",
uselist=False
)
def __init__(self, user, title):
assert type(user).__name__ == 'User', 'Не передан объект User'

View File

@@ -0,0 +1,42 @@
__author__ = 'RemiZOffAlex'
__email__ = 'remizoffalex@mail.ru'
import datetime
from sqlalchemy import Column, Integer, ForeignKey, DateTime, String
from sqlalchemy.orm import relationship
from .. import Base
class NoteTrash(Base):
"""Корзина для страниц справки
"""
__tablename__ = "note_trash"
id = Column(Integer, primary_key=True, index=True)
# ID пользователя
user_id = Column(Integer, ForeignKey('user.id'), index=True)
# ID страницы
note_id = Column(Integer, ForeignKey('note.id'), index=True)
# Дата удаления
created = Column(DateTime)
# Связи
user = relationship(
"User",
primaryjoin="NoteTrash.user_id == User.id",
uselist=False
)
note = relationship(
"Note",
primaryjoin="NoteTrash.note_id == Note.id",
back_populates="trash",
uselist=False
)
def __init__(self, user, note):
assert type(user).__name__ == 'User', 'Не передан объект User'
assert type(note).__name__ == 'Note', 'Не передан объект Note'
self.user_id = user.id
self.note_id = note.id
self.created = datetime.datetime.now()

View File

@@ -0,0 +1,49 @@
__author__ = 'RemiZOffAlex'
__email__ = 'remizoffalex@mail.ru'
import datetime
from sqlalchemy import Table, Column, Boolean, Integer, ForeignKey, String, DateTime
from sqlalchemy.orm import relationship
from .. import Base
class NoteTree(Base):
"""Справка по сервису
"""
__tablename__ = "note_tree"
id = Column(Integer, primary_key=True, index=True)
parent_id = Column(Integer, ForeignKey('note.id'), index=True) # ссылку на предка (ancestor)
child_id = Column(Integer, ForeignKey('note.id'), index=True) # ссылку на потомка (descendant)
level = Column(Integer, default=0) # Уровень относительно родителя
# Связи
# Родитель
parent = relationship(
"Note",
primaryjoin="Note.id == NoteTree.parent_id",
uselist=False
)
# Дочерний узел
child = relationship(
"Note",
primaryjoin="Note.id == NoteTree.child_id",
uselist=False
)
def __init__(self, parent, child):
assert type(parent).__name__ == 'Note', 'Не передан объект Note'
assert type(child).__name__ == 'Note', 'Не передан объект Note'
self.parent_id = parent.id
self.child_id = child.id
def __repr__(self):
return "<NoteTree({id}, '{parent}', '{child}')>".format(
id=self.id,
parent=self.parent,
child=self.child
)
def __json__(self):
return {c.name: getattr(self, c.name) for c in self.__table__.columns}

View File

@@ -28,6 +28,21 @@ class Page(Base):
uselist=False
)
tags = relationship("PageTag", primaryjoin="Page.id==PageTag.page_id")
# Родитель
parents = relationship(
"PageTree",
primaryjoin="Page.id == PageTree.child_id",
)
# Дочерние узлы
nodes = relationship(
"PageTree",
primaryjoin="Page.id == PageTree.parent_id",
)
trash = relationship(
"PageTrash",
primaryjoin="Page.id == PageTrash.page_id",
uselist=False
)
def __init__(self, user, title):
assert type(user).__name__ == 'User', 'Не передан объект User'

View File

@@ -0,0 +1,42 @@
__author__ = 'RemiZOffAlex'
__email__ = 'remizoffalex@mail.ru'
import datetime
from sqlalchemy import Column, Integer, ForeignKey, DateTime, String
from sqlalchemy.orm import relationship
from .. import Base
class PageTrash(Base):
"""Корзина для страниц справки
"""
__tablename__ = "page_trash"
id = Column(Integer, primary_key=True, index=True)
# ID пользователя
user_id = Column(Integer, ForeignKey('user.id'), index=True)
# ID страницы
page_id = Column(Integer, ForeignKey('page.id'), index=True)
# Дата удаления
created = Column(DateTime)
# Связи
user = relationship(
"User",
primaryjoin="PageTrash.user_id == User.id",
uselist=False
)
page = relationship(
"Page",
primaryjoin="PageTrash.page_id == Page.id",
back_populates="trash",
uselist=False
)
def __init__(self, user, page):
assert type(user).__name__ == 'User', 'Не передан объект User'
assert type(page).__name__ == 'Page', 'Не передан объект Page'
self.user_id = user.id
self.page_id = page.id
self.created = datetime.datetime.now()

View File

@@ -0,0 +1,49 @@
__author__ = 'RemiZOffAlex'
__email__ = 'remizoffalex@mail.ru'
import datetime
from sqlalchemy import Table, Column, Boolean, Integer, ForeignKey, String, DateTime
from sqlalchemy.orm import relationship
from .. import Base
class PageTree(Base):
"""Справка по сервису
"""
__tablename__ = "page_tree"
id = Column(Integer, primary_key=True, index=True)
parent_id = Column(Integer, ForeignKey('page.id'), index=True) # ссылку на предка (ancestor)
child_id = Column(Integer, ForeignKey('page.id'), index=True) # ссылку на потомка (descendant)
level = Column(Integer, default=0) # Уровень относительно родителя
# Связи
# Родитель
parent = relationship(
"Page",
primaryjoin="Page.id == PageTree.parent_id",
uselist=False
)
# Дочерний узел
child = relationship(
"Page",
primaryjoin="Page.id == PageTree.child_id",
uselist=False
)
def __init__(self, parent, child):
assert type(parent).__name__ == 'Page', 'Не передан объект Page'
assert type(child).__name__ == 'Page', 'Не передан объект Page'
self.parent_id = parent.id
self.child_id = child.id
def __repr__(self):
return "<PageTree({id}, '{parent}', '{child}')>".format(
id=self.id,
parent=self.parent,
child=self.child
)
def __json__(self):
return {c.name: getattr(self, c.name) for c in self.__table__.columns}

View File

@@ -34,6 +34,7 @@ jsonrpc = JSONRPC()
from . import ( # noqa F401
login,
favorite,
note,
profile,
page,

View File

@@ -0,0 +1,7 @@
__author__ = 'RemiZOffAlex'
__email__ = 'remizoffalex@mail.ru'
from . import (
note,
page
)

View File

@@ -0,0 +1,110 @@
__author__ = 'RemiZOffAlex'
__email__ = 'remizoffalex@mail.ru'
from .. import jsonrpc, login_required
from ... import acl, app, lib, models
from ...mutations.note import note_as_dict
@jsonrpc.method('favorite.note.add')
@login_required
def favorite_note_add(id: int) -> bool:
"""Добавление статьи в избранное
"""
note = models.db_session.query(
models.Note
).filter(
models.Note.id == id
).first()
if note is None:
raise ValueError
exist = models.db_session.query(
models.NoteFavorite
).filter(
models.NoteFavorite.note_id == id,
models.NoteFavorite.user_id == lib.get_user().id
).first()
if exist:
raise ValueError
new_favorite_note = models.NoteFavorite(
lib.get_user(),
note
)
models.db_session.add(new_favorite_note)
models.db_session.commit()
return True
@jsonrpc.method('favorite.note.delete')
@login_required
def favorite_note_delete(id: int) -> bool:
"""Удаление статьи из избранного
"""
exist = models.db_session.query(
models.NoteFavorite
).filter(
models.NoteFavorite.note_id == id,
models.NoteFavorite.user_id == lib.get_user().id
).first()
if exist is None:
raise ValueError
models.db_session.delete(exist)
models.db_session.commit()
return True
@jsonrpc.method('favorite.notes')
@login_required
def favorite_notes(
page: int = 1,
order_by: dict = {'field': 'title', 'order': 'asc'},
fields: list = ['id', 'title']
) -> list:
indexes = models.db_session.query(
models.NoteFavorite.note_id
).filter(
models.NoteFavorite.user_id == lib.get_user().id
)
notes = models.db_session.query(
models.Note
).filter(
models.Note.id.in_(indexes)
)
# Сортировка
if order_by['field'] not in ['created', 'id', 'title', 'updated']:
raise ValueError
if order_by['order'] not in ['asc', 'desc']:
raise ValueError
field = getattr(models.Note, order_by['field'])
order = getattr(field, order_by['order'])
notes = notes.order_by(
order()
)
notes = lib.getpage(
notes,
page,
app.config['ITEMS_ON_PAGE']
).all()
result = []
for note in notes:
newRow = note_as_dict(note, fields)
result.append(newRow)
return result
@jsonrpc.method('favorite.notes.count')
@login_required
def favorite_notes_count() -> int:
result = models.db_session.query(
models.NoteFavorite
).filter(
models.NoteFavorite.user_id == lib.get_user().id
).count()
return result

View File

@@ -0,0 +1,110 @@
__author__ = 'RemiZOffAlex'
__email__ = 'remizoffalex@mail.ru'
from .. import jsonrpc, login_required
from ... import acl, app, lib, models
from ...mutations.page import page_as_dict
@jsonrpc.method('favorite.page.add')
@login_required
def favorite_page_add(id: int) -> str:
"""Добавление статьи в избранное
"""
page = models.db_session.query(
models.Page
).filter(
models.Page.id == id
).first()
if page is None:
raise ValueError
exist = models.db_session.query(
models.PageFavorite
).filter(
models.PageFavorite.page_id == id,
models.PageFavorite.user_id == lib.get_user().id
).first()
if exist:
raise ValueError
new_favorite_page = models.PageFavorite(
lib.get_user(),
page
)
models.db_session.add(new_favorite_page)
models.db_session.commit()
return 'ok'
@jsonrpc.method('favorite.page.delete')
@login_required
def favorite_page_delete(id: int) -> str:
"""Удаление статьи из избранного
"""
exist = models.db_session.query(
models.PageFavorite
).filter(
models.PageFavorite.page_id == id,
models.PageFavorite.user_id == lib.get_user().id
).first()
if exist is None:
raise ValueError
models.db_session.delete(exist)
models.db_session.commit()
return 'ok'
@jsonrpc.method('favorite.pages')
@login_required
def favorite_pages(
page: int = 1,
order_by: dict = {'field': 'title', 'order': 'asc'},
fields: list = ['id', 'title']
) -> list:
indexes = models.db_session.query(
models.PageFavorite.page_id
).filter(
models.PageFavorite.user_id == lib.get_user().id
)
pages = models.db_session.query(
models.Page
).filter(
models.Page.id.in_(indexes)
)
# Сортировка
if order_by['field'] not in ['created', 'id', 'title', 'updated']:
raise ValueError
if order_by['order'] not in ['asc', 'desc']:
raise ValueError
field = getattr(models.Page, order_by['field'])
order = getattr(field, order_by['order'])
pages = pages.order_by(
order()
)
pages = lib.getpage(
pages,
page,
app.config['ITEMS_ON_PAGE']
).all()
result = []
for page in pages:
newRow = page_as_dict(page, fields)
result.append(newRow)
return result
@jsonrpc.method('favorite.pages.count')
@login_required
def favorite_pages_count() -> int:
result = models.db_session.query(
models.PageFavorite
).filter(
models.PageFavorite.user_id == lib.get_user().id
).count()
return result

View File

@@ -3,10 +3,14 @@ __email__ = 'remizoffalex@mail.ru'
from . import jsonrpc, login_required
from .. import app, lib, models
from ..mutations.note import note_as_dict
@jsonrpc.method('note')
def note_id(id: int) -> dict:
def note_id(
id: int,
fields: list[str] = ['id', 'title']
) -> dict:
"""Заметка
"""
note = models.db_session.query(
@@ -17,11 +21,7 @@ def note_id(id: int) -> dict:
if note is None:
raise ValueError
result = note.as_dict()
result['user'] = note.user.as_dict()
result['tags'] = []
for tagLink in note.tags:
result['tags'].append(tagLink.tag.as_dict())
result = note_as_dict(note, fields)
return result
@@ -91,14 +91,30 @@ def note_update(id: int, title: str, body: str) -> dict:
@jsonrpc.method('notes')
def notes_list(page: int) -> list:
def notes_list(
page: int = 1,
order_by: dict = {'field': 'title', 'order': 'asc'},
fields: list = ['id', 'title']
) -> list:
"""Список заметок
"""
notes = models.db_session.query(
models.Note
).order_by(
models.Note.title.asc()
).filter(
models.Note.user_id==lib.get_user().id
)
# Сортировка
if order_by['field'] not in ['created', 'id', 'title', 'status', 'updated']:
raise ValueError
if order_by['order'] not in ['asc', 'desc']:
raise ValueError
field = getattr(models.Note, order_by['field'])
order = getattr(field, order_by['order'])
notes = notes.order_by(
order()
)
notes = lib.getpage(
notes,
page,
@@ -107,11 +123,7 @@ def notes_list(page: int) -> list:
result = []
for note in notes:
newRow = note.as_dict()
newRow['user'] = note.user.as_dict()
newRow['tags'] = []
for tagLink in note.tags:
newRow['tags'].append(tagLink.tag.as_dict())
newRow = note_as_dict(note, fields)
result.append(newRow)
return result

View File

@@ -1,5 +1,6 @@
{% include '/components/backtotop.js' %}
{% include '/components/filter.js' %}
{% include '/components/footer.js' %}
{% include '/components/info.js' %}
{% include '/components/order_by.js' %}
{% include '/components/pagination.js' %}

View File

@@ -0,0 +1,39 @@
function ComponentInfo() {
let data = {
item: null,
}
return {
oninit: function(vnode) {
console.log('ComponentInfo.oninit');
for (let key in vnode.attrs){
data[key] = vnode.attrs[key];
};
},
onbeforeupdate: function(vnode) {
console.log('ComponentInfo.onbeforeupdate');
for (let key in vnode.attrs){
data[key] = vnode.attrs[key];
};
},
view: function(vnode) {
console.log('ComponentInfo.view');
let result = [];
if (data.item!=null) {
result.push(
m('div', {class: 'row'},
m('div', {class: 'col text-muted py-2'},
m('small', [
m('i', {class: 'fa fa-user me-1'}),
m(m.route.Link, {class: 'me-2', href: `/user/${data.item.user.id}`}, data.item.user.name),
{tag: "<", children: `Создано: ${data.item.created}`},
{tag: "<", children: '&ensp;'},
{tag: "<", children: `Обновлено: ${data.item.updated}`},
])
)
)
);
};
return result;
}
}
};

View File

@@ -1,4 +1,6 @@
{% include '/private/components/favorite.js' %}
{% include '/private/components/menu-general.js' %}
{% include '/private/components/notes.js' %}
{% include '/private/components/pages.js' %}
{% include '/private/components/tags.js' %}
{% include '/private/components/users.js' %}

View File

@@ -30,8 +30,10 @@ function MenuGeneral() {
m(m.route.Link, { class: 'btn btn-outline-secondary btn-lg border-0', href: '/tags'}, 'Метки'),
m(m.route.Link, { class: 'btn btn-outline-secondary btn-lg border-0', href: '/users'}, 'Пользователи'),
m('div', { class: 'btn-group btn-group-lg float-end'},
m(m.route.Link, { class: 'btn btn-outline-primary', href: '/notes' }, m('i', { class: 'fa fa-sticky-note-o' })),
m(m.route.Link, { class: 'btn btn-outline-warning', href: '/favorite' }, m('i', { class: 'fa fa-star' })),
m(m.route.Link, { class: 'btn btn-outline-secondary', href: '/profile'}, m('i', {class: 'fa fa-user'})),
m('button', {class: "btn btn-outline-danger", onclick: logout}, m('i', {class: "fa fa-sign-out"})),
m('button', {class: 'btn btn-outline-danger', onclick: logout}, m('i', {class: 'fa fa-sign-out'})),
)
])
)

View File

@@ -0,0 +1,57 @@
function ComponentNotes() {
let data = {
notes: null,
};
function note_render(note, noteIdx) {
let odd = '';
if (noteIdx % 2) {
odd = ' bg-light'
};
let tags = note.tags.map(
function(tag, tagIdx) {
return [
m('i', {class: "fa fa-tag"}),
{tag: '<', children: '&nbsp;'},
m(m.route.Link, {class: "font-monospace text-decoration-none", href: `/tag/${tag.id}`}, tag.name),
{tag: '<', children: '&nbsp;'},
]
}
);
return m('div', {class: 'row'},
m('div', {class: "col py-2" + odd}, [
m(m.route.Link, {class: "text-decoration-none", href: `/note/${note.id}`}, m.trust(note.title)),
m('div', {class: 'row'},
m('div', {class: 'col text-muted'},
m('small', [...tags])
)
)
])
)
};
function notes_render() {
return data.notes.map(note_render);
};
return {
oninit: function(vnode) {
console.log('ComponentNotes.oninit');
for (let key in vnode.attrs){
data[key] = vnode.attrs[key];
};
},
onupdate: function(vnode) {
console.log('ComponentNotes.onupdate');
for (let key in vnode.attrs){
data[key] = vnode.attrs[key];
};
},
view: function() {
console.log('ComponentNotes.view');
if (data.notes!=null) {
let result = [];
result.push(notes_render());
return result;
};
}
};
};

View File

@@ -0,0 +1,356 @@
/*
Пример
result.push(m(ComponentTags, {resource: data.file, typeOf: 'file'}));
*/
function ComponentTags(arguments) {
let data = {
resource: null,
typeOf: null,
newtag: '',
groups: {},
tags_all: [],
panels: {
standart: {
visible: false
},
new: {
visible: false
}
},
error: null,
};
for (let key in arguments){
data[key] = arguments[key];
};
function tag_add() {
/* Добавить тег к ресурсу */
let newtag = data.newtag.trim().toLowerCase();
data.newtag = newtag;
if (data.newtag.length<2) {
return;
}
m.request({
url: '/api',
method: "POST",
body: {
"jsonrpc": "2.0",
"method": 'tag.exist',
"params": {
"name": data.newtag
},
"id": get_id()
}
}).then(
function(response) {
if ('result' in response) {
tag_add_to_node( response['result'] );
} else if ('error' in response) {
console.log(response);
data.panels.new.visible = true;
}
}
).catch(
function(error) {
console.log(error);
}
);
};
function tag_new_add() {
/* Добавление нового тега */
if (data.newtag.length<2) {
return;
}
m.request({
url: '/api',
method: "POST",
body: {
"jsonrpc": "2.0",
"method": 'tag.add',
"params": {
"name": data.newtag
},
"id": get_id()
}
}).then(
function(response) {
if ('result' in response) {
tags_get();
tag_add_to_node( response['result'] );
}
}
);
};
function tag_remove(tag) {
/* Удаление тега из ресурса */
console.log(tag_includes(tag));
m.request({
url: '/api',
method: "POST",
body: {
"jsonrpc": "2.0",
"method": `tag.${data.typeOf}.delete`,
"params": {
"tag": tag.id,
"id": data.resource.id
},
"id": get_id()
}
}).then(
function(response) {
if ('result' in response) {
data.resource.tags = arrayRemove(data.resource.tags, tag);
}
}
)
};
function tags_get() {
/* Получить список тегов */
m.request({
url: '/api',
method: "POST",
body: [
{
"jsonrpc": "2.0",
"method": 'tags.groups',
"params": {},
"id": get_id()
},
{
"jsonrpc": "2.0",
"method": 'tags',
"params": {},
"id": get_id()
}
]
}).then(
function(response) {
if ('result' in response[0]) {
data.groups = response[0]['result'];
}
if ('result' in response[1]) {
data.tags_all = response[1]['result'];
}
}
)
};
function tags_filtered() {
let result = data.tags_all.filter(filterTag);
return result;
};
function tag_includes(tag) {
let result = data.resource.tags.find(function(element, index, array) {
return element.id===tag.id;
}, tag);
return result;
};
function filterTag(tag) {
let value = data.newtag;
if ( value.length<2 ) {
return false;
}
if ( tag.name.toLowerCase().includes(value.toLowerCase()) ) {
return true;
}
return false;
};
function tag_add_to_node(tag) {
/* Добавление тега к ресурсу */
m.request({
url: '/api',
method: "POST",
body: {
"jsonrpc": "2.0",
"method": `tag.${data.typeOf}.add`,
"params": {
"id": data.resource.id,
"tag": tag.id
},
"id": get_id()
}
}).then(
function(response) {
if ('result' in response) {
data.resource.tags.push(response['result']);
sortedTags();
data.panels.new.visible = false;
}
}
)
};
function sortedTags() {
if (data.resource.tags === undefined) {return [];}
data.resource.tags.sort(
function(a, b) {
if (a.name > b.name) {
return 1;
}
if (a.name < b.name) {
return -1;
}
return 0;
}
);
};
function newtag_clear() {
/* Очистка поля нового тега */
data.newtag = '';
data.panels.new.visible = false;
};
function form_submit_tag_add(e) {
e.preventDefault();
tag_add();
};
function form_submit_cancelNewTag(e) {
e.preventDefault();
newtag_clear();
};
function groups_render() {
let result = [];
Object.keys(data.groups).forEach(
function(group, groupIdx) {
if (groupIdx % 2) {
result.push(m('a', {class: 'btn btn-outline-secondary font-monospace btn-lg me-2 mb-2', href: `#tag${groupIdx}`}, group));
} else {
result.push(m('a', {class: 'btn btn-outline-primary font-monospace btn-lg me-2 mb-2', href: `#tag${groupIdx}`}, group));
};
}
);
return result;
};
function groups_tags_render() {
let result = [];
Object.keys(data.groups).forEach(
function(group, groupIdx) {
let tags = data.groups[group].map(
function(tag, tagIdx) {
if (tag_includes(tag)) {
return m('button', {type: 'button', class: 'btn btn-primary font-monospace me-2 mb-1', onclick: function() {tag_remove(tag)}}, tag.name);
} else {
return m('button', {type: 'button', class: 'btn btn-outline-secondary font-monospace me-2 mb-1', onclick: function() {tag_add_to_node(tag)}}, tag.name);
}
}
);
result.push(m('div', {class: 'row mt-3'},
m('div', {class: 'col pe-0'}, [
m('a', {name: `tag${groupIdx}`}),
m('a', {class: 'btn btn-outline-danger me-2 mb-1', href: '#tags'}, group),
[...tags]
])
));
}
);
return result;
};
function tags_render() {
let result = data.resource.tags.map(
function(tag) {
return m('div', {class: 'btn-group mb-1 me-2'}, [
m(m.route.Link, {class: 'btn btn-outline-secondary font-monospace', href: `/tag/${tag.id}`}, tag.name),
m('button', {class: 'btn btn-outline-danger', type: 'button', onclick: function() {tag_remove(tag)}}, m('i', {class: 'fa fa-remove'})),
]);
}
);
return result;
};
function button_add_render() {
if (data.panels.standart.visible) {
return m('button', {class: 'btn btn-outline-danger mb-1 me-2', type: 'button', onclick: function() {panel_show(data.panels.standart)}}, m('i', {class: 'fa fa-minus'}));
} else {
return m('button', {class: 'btn btn-outline-success mb-1 me-2', type: 'button', onclick: function() {panel_show(data.panels.standart)}}, m('i', {class: 'fa fa-plus'}));
}
};
return {
data: data,
oninit: function(vnode) {
console.log('ComponentTags.oninit');
for (let key in vnode.attrs){
data[key] = vnode.attrs[key];
};
tags_get();
},
onbeforeupdate: function(vnode) {
console.log('ComponentTags.onbeforeupdate');
for (let key in vnode.attrs){
data[key] = vnode.attrs[key];
};
// tags_get();
},
view: function(vnode) {
console.log('ComponentTags.view');
let result = []
if (data.resource) {
result.push(
m('div', {class: 'row mb-3'},
m('div', {class: 'col'}, [
m('a', {name: "tags"}),
m('div', {class: 'btn mb-1'}, m('i', {class: 'fa fa-tags'})),
button_add_render(),
tags_render()
])
)
);
if (data.panels.standart.visible) {
result.push(...[
m('div', {class: 'row mb-3'},
m('div', {class: 'col'}, [
m('form', {onsubmit: form_submit_tag_add},
m('div', {class: 'input-group'}, [
m('button', {class: 'btn btn-outline-danger', type: 'button', onclick: newtag_clear}, m('i', {class: 'fa fa-remove'})),
m('input', {class: 'form-control', oninput: function(e) {data.newtag = e.target.value}, onkeyup: function(e) { if (e.keyCode==13) { tag_add() }}, value: data.newtag}),
m('button', {class: 'btn btn-outline-success', type: 'submit'}, m('i', {class: 'fa fa-save'})),
])
)
])
),
m('div', {class: 'row mb-3'},
m('div', {class: 'col py-2'},
(function() {
let result = tags_filtered().map(
function(tag) {
if (tag_includes(tag)) {
return m('button', {class: 'btn btn-primary font-monospace me-2 mb-1', type: 'button', onclick: function() { tag_remove(tag) }}, tag.name);
} else {
return m('button', {class: 'btn btn-outline-secondary font-monospace me-2 mb-1', type: 'button', onclick: function() { tag_add_to_node(tag) }}, tag.name);
}
}
);
return result;
})()
)
),
m('div', {class: 'row mb-3'},
m('div', {class: 'col py-2'},
groups_render()
)
)
]);
if (data.panels.new.visible) {
result.push(
m('form', {onsubmit: form_submit_cancelNewTag},
m('div', {class: 'row mb-3'},
m('div', {class: 'col py-2'}, [
m('label', 'Добавить новый тег?'),
m('div', {class: 'row'},
m('div', {class: 'col py-2'},
m('div', {class: 'input-group'}, [
m('button', {class: 'btn btn-outline-danger', type: 'submit'}, m('i', {class: 'fa fa-close'})),
m('input', {class: 'form-control', oninput: function (e) {data.newtag = e.target.value}, value: data.newtag}),
m('button', {class: 'btn btn-outline-success', type: 'button', onclick: tag_new_add}, 'Добавить')
])
)
)
])
)
),
);
};
result.push(
groups_tags_render()
);
};
};
return result;
}
};
};

View File

@@ -0,0 +1,110 @@
function Favorite() {
function breadcrumbs_render() {
let result = m('ul', {class: 'breadcrumb mt-2'}, [
m('li', {class: 'breadcrumb-item'}, m(m.route.Link, {href: '/'}, m('i', {class: 'fa fa-home'}))),
m('li', {class: 'breadcrumb-item'}, m(m.route.Link, {href: '/favorite'}, 'Избранное')),
m('li', {class: 'breadcrumb-item active'}, 'Избранные вопросы'),
]);
return result;
};
return {
view: function(vnode) {
console.log('Favorite.view');
let result = [];
result.push([
breadcrumbs_render(),
m('div', {class: 'row'},
m('div', {class: 'col h1 py-2'}, [
m(m.route.Link, {class: "btn btn-outline-secondary btn-lg me-2", href: '/profile', title: "Вернуться"}, m('i', {class: "fa fa-chevron-left"})),
'Избранное'
])
),
m('hr'),
m(MenuFavorite),
breadcrumbs_render(),
]);
return result;
}
};
};
/*
<h3>
<a class="btn btn-outline-secondary" href="/profile"><i class="fa fa-chevron-left"></i></a>
Избранное</h3>
{# include 'favorite_menu.html' #}
<hr />
<div class='row'>
<div class="col-md-4 py-2">
<a class="btn btn-outline-secondary btn-lg w-100" href="/favorite/questions">Вопросы <span class="badge bg-secondary">{ questions }</span></a>
</div>
<div class="col-md-4 py-2">
<a class="btn btn-outline-secondary btn-lg w-100" href="/favorite/orders">Заказы <span class="badge bg-secondary">{ orders }</span></a>
</div>
<div class="col-md-4 py-2">
<a class="btn btn-outline-secondary btn-lg w-100" href="/favorite/pages">Статьи <span class="badge bg-secondary">{ pages }</span></a>
</div>
<div class="col-md-4 py-2">
<a class="btn btn-outline-secondary btn-lg w-100" href="/favorite/resumes">Резюме <span class="badge bg-secondary">{ resumes }</span></a>
</div>
</div>
Object.assign(root.data, {
questions: 0,
orders: 0,
pages: 0,
resumes: 0,
menuitem: null,
});
root.created = function() {
let vm = this;
m.request({
url: '/api',
method: "POST",
body: [
{
"jsonrpc": "2.0",
"method": 'favorite.questions.count',
"params": {},
"id": get_id()
},
{
"jsonrpc": "2.0",
"method": 'favorite.orders.count',
"params": {},
"id": get_id()
},
{
"jsonrpc": "2.0",
"method": 'favorite.pages.count',
"params": {},
"id": get_id()
},
{
"jsonrpc": "2.0",
"method": 'favorite.resumes.count',
"params": {},
"id": get_id()
},
]
}).then(
function(response) {
if ('result' in response[0]) {
vm.questions = response[0]['result'];
}
if ('result' in response[1]) {
vm.orders = response[1]['result'];
}
if ('result' in response[2]) {
vm.pages = response[2]['result'];
}
if ('result' in response[3]) {
vm.resumes = response[3]['result'];
}
}
);
};
*/

View File

@@ -0,0 +1,15 @@
{% include '/private/domains/favorite/favorite.js' %}
{% include '/private/domains/favorite/menu.js' %}
{% include '/private/domains/favorite/notes.js' %}
{% include '/private/domains/favorite/pages.js' %}
Object.assign(
routes,
{
"/favorite": layout_decorator(Favorite),
"/favorite/notes": layout_decorator(FavoriteNotes),
"/favorite/notes/:page": layout_decorator(FavoriteNotes),
"/favorite/pages": layout_decorator(FavoritePages),
"/favorite/pages/:page": layout_decorator(FavoritePages),
}
);

View File

@@ -0,0 +1,45 @@
function MenuFavorite() {
let data = {
menuitem: null,
}
function button_common() {
if (data.menuitem===null) {
return {tag: '<', children: '<div class="btn btn-primary me-2"><i class="fa fa-bars"></i></div>'};
} else {
return m(m.route.Link, {class: "btn btn-outline-secondary me-2 text-decoration-none", href: '/favorite', title: 'Избранное'}, m('i', {class: 'fa fa-bars'}))
}
};
function button_pages() {
if (data.menuitem==='pages') {
return {tag: '<', children: '<div class="btn btn-primary me-2">Статьи</div>'};
} else {
return m(m.route.Link, {class: "btn btn-outline-secondary me-2 text-decoration-none", href: '/favorite/pages'}, 'Статьи')
}
};
function button_notes() {
if (data.menuitem === 'notes') {
return { tag: '<', children: '<div class="btn btn-primary me-2">Заметки</div>' };
} else {
return m(m.route.Link, { class: "btn btn-outline-secondary me-2 text-decoration-none", href: '/favorite/notes' }, 'Заметки')
}
};
return {
oninit: function(vnode) {
console.log('MenuFavorite.oninit');
for (let key in vnode.attrs){
console.log(key);
data[key] = vnode.attrs[key];
};
},
view: function() {
console.log('MenuFavorite.view');
return m('div', {class: 'row'},
m('div', {class: 'col py-2'}, [
button_common(),
button_notes(),
button_pages(),
])
);
}
}
};

View File

@@ -0,0 +1,120 @@
function FavoriteNotes() {
let data = {
filter: PanelFilter(),
order_by: PanelOrderBy({
field: 'updated',
fields: [
{value: 'id', text: 'ID'},
{value: 'title', text: 'заголовку'},
{value: 'created', text: 'дате создания'},
{value: 'updated', text: 'дате обновления'}
],
clickHandler: notes_get,
order: 'asc',
}),
notes: [],
pagination: {
page: 1,
size: 0,
prefix_url: '/favorite/notes'
},
};
function breadcrumbs_render() {
let result = m('ul', {class: 'breadcrumb mt-2'}, [
m('li', {class: 'breadcrumb-item'}, m(m.route.Link, {href: '/'}, m('i', {class: 'fa fa-home'}))),
m('li', {class: 'breadcrumb-item'}, m(m.route.Link, {href: '/favorite'}, 'Избранное')),
m('li', {class: 'breadcrumb-item active'}, 'Избранные заметки'),
]);
return result;
};
function notes_get() {
m.request({
url: '/api',
method: "POST",
body: [
{
"jsonrpc": "2.0",
"method": 'favorite.notes',
"params": {
"page": data.pagination.page,
"order_by": data.order_by.value,
"fields": ["id", "title", "tags", "created", "updated", "user"]
},
"id": get_id()
},
{
"jsonrpc": "2.0",
"method": 'favorite.notes.count',
"id": get_id()
}
]
}).then(
function(response) {
if ('result' in response[0]) {
data.notes = response[0]['result'];
}
if ('result' in response[1]) {
data.pagination.size = response[1]['result'];
}
}
);
};
return {
oninit: function(vnode) {
console.log('FavoriteNotes.oninit');
document.title = `Избранные статьи - ${SETTINGS.TITLE}`;
if (vnode.attrs.page!==undefined) {
data.pagination.page = Number(vnode.attrs.page);
};
notes_get();
},
onbeforeupdate: function(vnode) {
console.log('FavoriteNotes.onbeforeupdate');
if (vnode.attrs.page!==undefined) {
if (data.pagination.page != Number(vnode.attrs.page)) {
data.pagination.page = Number(vnode.attrs.page);
notes_get();
};
} else {
if (data.pagination.page != 1) {
data.pagination.page = 1;
notes_get();
};
}
},
view: function(vnode) {
console.log('FavoriteNotes.view');
result = [];
result.push(
breadcrumbs_render(),
m('div', {class: 'row'},
m('div', {class: 'col h1 py-2'}, [
m('div', {class: "btn-group btn-group-lg me-2"}, [
m(m.route.Link, {class: "btn btn-outline-secondary", href: '/favorite', title: "Вернуться"}, m('i', {class: "fa fa-chevron-left"})),
m('button', {type: "button", class: "btn btn-outline-secondary", onclick: function() { panel_show(data.filter.data) }},
m('i', {class: "fa fa-filter"})
),
m('button', {type: "button", class: "btn btn-outline-secondary", onclick: function() { panel_show(data.order_by.data) }},
m('i', {class: "fa fa-sort-alpha-asc"})
)
]),
`Избранные заметки`
])
),
m('hr'),
m(MenuFavorite, {menuitem: 'notes'}),
);
result.push(m(data.filter));
result.push(m(data.order_by));
result.push(m(Pagination, data.pagination));
if (data.notes.length>0) {
result.push(m(ComponentNotes, {notes: data.notes}));
result.push(m(Pagination, data.pagination));
};
result.push(breadcrumbs_render());
return result;
}
};
};

View File

@@ -0,0 +1,120 @@
function FavoritePages() {
let data = {
filter: PanelFilter(),
order_by: PanelOrderBy({
field: 'updated',
fields: [
{value: 'id', text: 'ID'},
{value: 'title', text: 'заголовку'},
{value: 'created', text: 'дате создания'},
{value: 'updated', text: 'дате обновления'}
],
clickHandler: pages_get,
order: 'asc',
}),
pages: [],
pagination: {
page: 1,
size: 0,
prefix_url: '/favorite/pages'
},
};
function breadcrumbs_render() {
let result = m('ul', {class: 'breadcrumb mt-2'}, [
m('li', {class: 'breadcrumb-item'}, m(m.route.Link, {href: '/'}, m('i', {class: 'fa fa-home'}))),
m('li', {class: 'breadcrumb-item'}, m(m.route.Link, {href: '/favorite'}, 'Избранное')),
m('li', {class: 'breadcrumb-item active'}, 'Избранные статьи'),
]);
return result;
};
function pages_get() {
m.request({
url: '/api',
method: "POST",
body: [
{
"jsonrpc": "2.0",
"method": 'favorite.pages',
"params": {
"page": data.pagination.page,
"order_by": data.order_by.value,
"fields": ["id", "title", "tags", "created", "updated", "user"]
},
"id": get_id()
},
{
"jsonrpc": "2.0",
"method": 'favorite.pages.count',
"id": get_id()
}
]
}).then(
function(response) {
if ('result' in response[0]) {
data.pages = response[0]['result'];
}
if ('result' in response[1]) {
data.pagination.size = response[1]['result'];
}
}
);
};
return {
oninit: function(vnode) {
console.log('FavoritePages.oninit');
document.title = `Избранные статьи - ${SETTINGS.TITLE}`;
if (vnode.attrs.page!==undefined) {
data.pagination.page = Number(vnode.attrs.page);
};
pages_get();
},
onbeforeupdate: function(vnode) {
console.log('FavoritePages.onbeforeupdate');
if (vnode.attrs.page!==undefined) {
if (data.pagination.page != Number(vnode.attrs.page)) {
data.pagination.page = Number(vnode.attrs.page);
pages_get();
};
} else {
if (data.pagination.page != 1) {
data.pagination.page = 1;
pages_get();
};
}
},
view: function(vnode) {
console.log('FavoritePages.view');
result = [];
result.push(
breadcrumbs_render(),
m('div', {class: 'row'},
m('div', {class: 'col h1 py-2'}, [
m('div', {class: "btn-group btn-group-lg me-2"}, [
m(m.route.Link, {class: "btn btn-outline-secondary", href: '/favorite', title: "Вернуться"}, m('i', {class: "fa fa-chevron-left"})),
m('button', {type: "button", class: "btn btn-outline-secondary", onclick: function() { panel_show(data.filter.data) }},
m('i', {class: "fa fa-filter"})
),
m('button', {type: "button", class: "btn btn-outline-secondary", onclick: function() { panel_show(data.order_by.data) }},
m('i', {class: "fa fa-sort-alpha-asc"})
)
]),
`Избранные статьи`
])
),
m('hr'),
m(MenuFavorite, {menuitem: 'pages'}),
);
result.push(m(data.filter));
result.push(m(data.order_by));
result.push(m(Pagination, data.pagination));
if (data.pages.length>0) {
result.push(m(ComponentPages, {pages: data.pages}));
result.push(m(Pagination, data.pagination));
};
result.push(breadcrumbs_render());
return result;
}
};
};

View File

@@ -1,3 +1,5 @@
{% include '/private/domains/favorite/inc.j2' %}
{% include '/private/domains/note/inc.j2' %}
{% include '/private/domains/page/inc.j2' %}
{% include '/private/domains/profile/inc.j2' %}
{% include '/private/domains/tag/inc.j2' %}

View File

@@ -0,0 +1,103 @@
function NoteAdd() {
let data = {
uuid: get_id(),
note: {
title: '',
body: '',
},
editor: null,
};
function breadcrumbs_render() {
let result = m('ul', {class: 'breadcrumb mt-2'}, [
m('li', {class: 'breadcrumb-item'}, m(m.route.Link, {href: '/'}, m('i', {class: 'fa fa-home'}))),
m('li', {class: 'breadcrumb-item'}, m(m.route.Link, {href: '/notes'}, 'Список заметок')),
m('li', {class: 'breadcrumb-item active'}, 'Новая заметка'),
]);
return result;
};
function form_submit(e) {
e.preventDefault();
note_add();
};
function note_add() {
if (data.note.title.length<2) {
return;
};
m.request({
url: '/api',
method: "POST",
body: {
"jsonrpc": "2.0",
"method": 'note.add',
"params": data.note,
"id": get_id()
}
}).then(
function(response) {
if ('result' in response) {
m.route.set(`/note/${response['result'].id}`);
}
}
)
};
function editor_events(ed) {
ed.on('change', function (e) {
data.note.body = ed.getContent();
});
};
return {
oncreate(vnode) {
console.log('NoteAdd.oncreate');
if (data.editor==null) {
tinymce_config = tinymce_config_init();
tinymce_config.selector = `#note_body_${data.uuid}`;
tinymce_config.setup = editor_events;
tinymce.init(tinymce_config).then(
function (editors) {
data.editor = editors[0];
}
);
}
},
onbeforeremove: function(vnode) {
console.log('NoteAdd.onbeforeremove');
if (data.editor!=null) {
data.editor.remove();
data.editor = null;
};
},
view: function(vnode) {
console.log('NoteAdd.view');
result = [];
result.push(breadcrumbs_render());
result.push([
m('div', {class: 'row'},
m('div', {class: 'col h1 py-2'}, [
m(m.route.Link, {class: "btn btn-outline-secondary btn-lg me-2", href: "/notes", title: "Список заметок"}, m('i', {class: 'fa fa-chevron-left'})),
'Новая заметка'
])
),
m('hr')
]);
result.push(
m('form', {onsubmit: form_submit}, [
m('div', {class: 'mb-2'}, [
m('label', {class: 'form-label'}, 'Заголовок'),
m('input', {class: 'form-control', type: 'text', oninput: function (e) {data.note.title = e.target.value}, value: data.note.title}, 'Заголовок'),
]),
m('div', {class: 'mb-2'}, [
m('label', {class: 'form-label'}, 'Текст'),
m('textarea', { class: 'form-control', cols: '40', rows: '8', id: `note_body_${data.uuid}`, oninput: function (e) {data.note.body = e.target.value}, value: data.note.body})
]),
m('div', {class: 'row'},
m('div', {class: 'col py-2'}, [
m('button', {class: 'btn btn-outline-success btn-lg float-end', type: 'submit'}, [m('i', {class: 'fa fa-save me-2'}), 'Сохранить']),
])
),
])
);
result.push(breadcrumbs_render());
return result;
}
};
};

View File

@@ -0,0 +1,170 @@
function NoteEdit() {
let data = {
uuid: get_id(),
note: null,
editor: null,
};
function breadcrumbs_render() {
let result = m('ul', {class: 'breadcrumb mt-2'}, [
m('li', {class: 'breadcrumb-item'}, m(m.route.Link, {href: '/'}, m('i', {class: 'fa fa-home'}))),
m('li', {class: 'breadcrumb-item'}, m(m.route.Link, {href: '/notes'}, 'Список статей')),
m('li', {class: 'breadcrumb-item'}, m(m.route.Link, {href: `/note/${data.note.id}`}, data.note.title)),
m('li', {class: 'breadcrumb-item active'}, 'Редактирование страницы'),
]);
return result;
};
function note_get(id) {
m.request({
url: '/api',
method: "POST",
body: {
"jsonrpc": "2.0",
"method": 'note',
"params": {
"id": id,
"fields": ["id", "title", "body"]
},
"id": get_id()
}
}).then(
function(response) {
if ('result' in response) {
data.note = response['result'];
}
}
)
};
function apply() {
/* Сохранить */
if (data.note.title.length<2) {
return;
};
m.request({
url: '/api',
method: "POST",
body: {
"jsonrpc": "2.0",
"method": "note.update",
"params": {
"id": data.note.id,
"title": data.note.title,
"body": data.note.body
},
"id": get_id()
}
}).then(
function(response) {
if ('result' in response) {
}
}
);
};
function apply_and_close() {
/* Сохранить */
if (data.note.title.length<2) {
return;
};
m.request({
url: '/api',
method: "POST",
body: {
"jsonrpc": "2.0",
"method": "note.update",
"params": {
"id": data.note.id,
"title": data.note.title,
"body": data.note.body
},
"id": get_id()
}
}).then(
function(response) {
if ('result' in response) {
m.route.set(`/note/${data.note.id}`);
}
}
);
};
function form_submit(e) {
e.preventDefault();
apply_and_close();
};
function editor_events(ed) {
ed.on('change', function (e) {
data.note.body = ed.getContent();
});
};
return {
oncreate(vnode) {
console.log('NoteEdit.oncreate');
if (data.note!=null) {
console.log(data.note);
if (data.editor==null) {
tinymce_config = tinymce_config_init();
tinymce_config.selector = `#note_body_${data.uuid}`;
tinymce_config.setup = editor_events;
tinymce.init(tinymce_config).then(
function (editors) {
data.editor = editors[0];
}
);
}
}
},
oninit: function(vnode) {
console.log('NoteEdit.oninit');
note_get(vnode.attrs.id);
},
onupdate: function(vnode) {
console.log('NoteEdit.onupdate');
if (data.note.id.toString()!==vnode.attrs.id) {
note_get(vnode.attrs.id);
};
if (data.note!=null) {
if (data.editor==null) {
tinymce_config = tinymce_config_init();
tinymce_config.selector = `#note_body_${data.uuid}`;
tinymce_config.setup = editor_events;
tinymce.init(tinymce_config).then(
function (editors) {
data.editor = editors[0];
}
);
};
};
},
view: function(vnode) {
let result = [];
if (data.note!=null) {
result.push(
breadcrumbs_render(),
m('div', {class: 'row'},
m('div', {class: 'col h1 py-2'},
m(m.route.Link, {class: "btn btn-outline-secondary btn-lg me-2", href: `/note/${data.note.id}`, title: data.note.title}, m('i', {class: 'fa fa-chevron-left'})),
'Редактирование страницы',
)
),
m('hr'),
m('form', {onsubmit: form_submit}, [
m('div', {class: 'mb-2'}, [
m('label', {class: 'form-label'}, 'Заголовок'),
m('input', {class: 'form-control', type: 'text', oninput: function (e) {data.note.title = e.target.value}, value: data.note.title}, 'Заголовок'),
]),
m('div', {class: 'mb-2'}, [
m('label', {class: 'form-label'}, 'Текст'),
m('textarea', { class: 'form-control', cols: '40', rows: '8', id: `note_body_${data.uuid}`, oninput: function (e) {data.note.body = e.target.value}, value: data.note.body})
]),
m('div', {class: 'row'},
m('div', {class: 'col py-2'}, [
m('button', { class: 'btn btn-outline-success btn-lg float-end', type: 'button', onclick: apply }, [m('i', { class: 'fa fa-save me-2'}), 'Сохранить']),
m('button', {class: 'btn btn-outline-success btn-lg float-end', type: 'submit'}, [m('i', {class: 'fa fa-save me-2'}), 'Сохранить и закрыть']),
])
),
])
);
result.push(breadcrumbs_render());
};
return result;
}
};
};

View File

@@ -0,0 +1,17 @@
{% include '/private/domains/note/add.js' %}
{% include '/private/domains/note/edit.js' %}
{% include '/private/domains/note/note.js' %}
{% include '/private/domains/note/notes.js' %}
{% include '/private/domains/note/source.js' %}
Object.assign(
routes,
{
"/note/add": layout_decorator(NoteAdd),
"/note/:id/edit": layout_decorator(NoteEdit),
"/note/:id/source": layout_decorator(NoteSource),
"/note/:id": layout_decorator(Note),
"/notes": layout_decorator(Notes),
"/notes/:page": layout_decorator(Notes),
}
);

View File

@@ -0,0 +1,220 @@
function Note() {
let data = {
note: null,
panels: {
standart: {
visible: false
}
},
};
function breadcrumbs_render() {
let result = m('ul', {class: 'breadcrumb mt-2'}, [
m('li', {class: 'breadcrumb-item'}, m(m.route.Link, {href: '/'}, m('i', {class: 'fa fa-home'}))),
m('li', {class: 'breadcrumb-item'}, m(m.route.Link, {href: '/notes'}, 'Список заметок')),
m('li', {class: 'breadcrumb-item active'}, data.note.title),
]);
return result;
};
function settings_save(name) {
if (name==='status') {
m.request({
url: '/api',
method: "POST",
body: {
"jsonrpc": "2.0",
"method": 'note.status',
"params": {
"id": data.note.id,
"status": data.note.status
},
"id": get_id()
}
}).then(
function(response) {
if ('result' in response) {
data.note = response['result'];
}
}
);
} else if (name==='typeOf') {
m.request({
url: '/api',
method: "POST",
body: {
"jsonrpc": "2.0",
"method": 'note.settings.update',
"params": {
"id": data.note.id,
"name": name,
"typeOf": "string",
"value": value
},
"id": get_id()
}
}).then(
function(response) {
if ('result' in response) {
data.note = response['result'];
}
}
);
}
};
function note_delete() {
/* Удалить статью в корзину */
m.request({
url: '/api',
method: "POST",
body: {
"jsonrpc": "2.0",
"method": 'note.delete',
"params": {
"id": data.note.id
},
"id": get_id()
}
}).then(
function(response) {
if ('result' in response) {
m.route.set('/notes');
}
}
);
};
function note_get(id) {
m.request({
url: '/api',
method: "POST",
body: [
{
"jsonrpc": "2.0",
"method": 'note',
"params": {
"id": id,
"fields": ["id", "title", "body", "created", "updated", "parent_id", "tags", "user", "nodes"]
},
"id": get_id()
},
{
"jsonrpc": "2.0",
"method": 'note.nodes',
"params": {
"id": id
},
"id": get_id()
}
]
}).then(
function(response) {
if ('result' in response[0]) {
data.note = response[0]['result'];
document.title = `${data.note.title} - ${SETTINGS.TITLE}`;
}
}
)
};
function button_back_render() {
if (data.note.parent_id===null) {
return m(m.route.Link, {class: "btn btn-outline-secondary", href: "/notes", title: "Список статей"}, m('i', {class: 'fa fa-chevron-left'}))
} else {
return m(m.route.Link, {class: "btn btn-outline-secondary", href: `/note/${data.note.parent_id}`, title: "Список статей"}, m('i', {class: 'fa fa-chevron-left'}))
}
};
function button_draft_render() {
if (data.note.status==='draft') {
return m('span', {class: 'small text-muted'}, 'черновик')
}
};
function button_trash_render() {
if (data.note.trash) {
return m('button', {class: 'btn btn-outline-success', onclick: note_recovery}, [m('i', {class: 'fa fa-plus'}), ' Восстановить из корзины'])
} else {
return m('button', {class: 'btn btn-outline-warning', onclick: note_delete}, [m('i', {class: 'fa fa-trash-o'}), ' В корзину'])
}
};
function panels_standart_render() {
if (data.panels.standart.visible) {
return m('div', {class: 'row'},
m('div', {class: 'col py-2'}, [
m(ComponentFavorite, {resource: data.note, name: 'note'}),
m(m.route.Link, {class: 'btn btn-outline-secondary', href: `/note/${data.note.id}/edit`}, [m('i', {class: 'fa fa-edit'}), ' Редактировать']),
m(m.route.Link, {class: 'btn btn-outline-secondary', href: `/note/${data.note.id}/moving`}, [m('i', {class: 'fa fa-arrows'}), ' Перемещение']),
m(m.route.Link, {class: 'btn btn-outline-secondary', href: `/note/${data.note.id}/history`}, [m('i', {class: 'fa fa-history'}), ' История изменений']),
m(m.route.Link, {class: 'btn btn-outline-secondary', href: `/note/${data.note.id}/source`}, [m('i', {class: 'fa fa-code'}), ' Исходный код']),
m(m.route.Link, {class: 'btn btn-outline-secondary', href: `/note/${data.note.id}/print`}, [m('i', {class: 'fa fa-print'}), ' Печать']),
m(m.route.Link, {class: 'btn btn-outline-secondary', href: `/note/${data.note.id}/recovery`}, [m('i', {class: 'fa fa-print'}), ' Печать']),
button_trash_render(),
m('h3', 'Свойства'),
m('div', {class: 'row py-2'},
m('div', {class: 'col-md-4'}, m('label', 'Статус')),
m('div', {class: 'col-md-8'},
m('div', {class: 'input-group'}, [
m('button', {class: 'btn btn-outline-warning', type: 'button'}, m('i', {class: 'fa fa-rotate-left'})),
m('select', {class: 'form-select', onchange: function(e) { data.note.status = e.target.value; }, value: data.note.status}, [
m('option', {value: 'draft'}, 'Черновик'),
m('option', {value: 'published'}, 'Опубликовано')
]),
m('button', {class: 'btn btn-outline-success', type: 'button', onclick: function() { settings_save('status') }}, m('i', {class: 'fa fa-save'}))
])
)
)
])
);
}
};
return {
oninit: function(vnode) {
console.log('Note.oninit');
note_get(Number(vnode.attrs.id));
},
onbeforeupdate: function(vnode) {
console.log('Note.onbeforeupdate');
if (data.note != null) {
if (data.note.id!==Number(vnode.attrs.id)) {
note_get(Number(vnode.attrs.id));
}
}
},
view: function(vnode) {
console.log('Note.view');
result = [];
if (data.note!=null) {
result.push(
breadcrumbs_render(),
m('div', {class: 'row'},
m('div', {class: 'col h1 py-2'},
m(m.route.Link, {class: 'btn btn-outline-success float-end', href: `/note/${data.note.id}/add`}, m('i', {class: 'fa fa-plus'})),
m('div', {class: 'btn-group btn-group-lg me-2'}, [
button_back_render(),
m('button', {type: 'button', class: 'btn btn-outline-secondary', onclick: function() {panel_show(data.panels.standart)}}, m('i', {class: 'fa fa-cog'})),
]),
data.note.title,
button_draft_render(),
)
),
panels_standart_render(),
m('hr'),
m(ComponentInfo, {item: data.note}),
m(ComponentTags, {resource: data.note, typeOf: 'note'}),
m('div', {class: 'row'},
m('div', {class: 'col'}, m.trust(data.note.body))
),
);
if (data.note.nodes.length>0) {
let nodes = data.note.nodes.map(
function(subnote, subnoteIdx) {
return m('li',
m(m.route.Link, {href: `/note/${subnote.id}`}, subnote.title)
);
}
);
result.push(
m('ul', [...nodes])
);
}
result.push(breadcrumbs_render());
}
return result;
}
}
};

View File

@@ -0,0 +1,112 @@
function Notes() {
let data = {
filter: PanelFilter(),
order_by: PanelOrderBy({
field: 'title',
fields: [
{value: 'id', text: 'ID'},
{value: 'title', text: 'заголовку'},
{value: 'created', text: 'дате создания'},
{value: 'updated', text: 'дате обновления'}
],
clickHandler: notes_get,
order: 'asc',
}),
notes: [],
pagination: {
page: 1,
size: 0,
prefix_url: '/notes'
},
}
function breadcrumbs_render() {
return m('ul', {class: 'breadcrumb mt-2'}, [
m('li', {class: 'breadcrumb-item'}, m(m.route.Link, {href: '/'}, m('i', {class: 'fa fa-home'}))),
m('li', {class: 'breadcrumb-item active'}, 'Список заметок')
]);
};
function notes_get() {
m.request({
url: '/api',
method: "POST",
body: [
{
"jsonrpc": "2.0",
"method": 'notes',
"params": {
"page": data.pagination.page,
"order_by": data.order_by.value,
"fields": ["id", "title", "tags", "created", "updated"]
},
"id": get_id()
},
{
"jsonrpc": "2.0",
"method": 'notes.count',
"params": {},
"id": get_id()
}
]
}).then(
function(response) {
if ('result' in response[0]) {
data.notes = response[0]['result'];
}
if ('result' in response[1]) {
data.pagination.size = response[1]['result'];
}
}
);
}
return {
oninit: function(vnode) {
console.log('Notes.oninit');
document.title = `Список статей - ${SETTINGS.TITLE}`;
if (vnode.attrs.page!==undefined) {
data.pagination.page = Number(vnode.attrs.page);
};
notes_get();
},
onbeforeupdate: function(vnode) {
console.log('Notes.onbeforeupdate');
if (vnode.attrs.page!==undefined) {
if (data.pagination.page.toString() != vnode.attrs.page) {
data.pagination.page = Number(vnode.attrs.page);
notes_get();
}
};
},
view: function(vnode) {
console.log('Notes.view');
result = [];
result.push([
breadcrumbs_render(),
m('div', {class: 'row'},
m('div', {class: 'col py-2 h1'}, [
m(m.route.Link, {class: 'btn btn-outline-success btn-lg float-end', href: '/note/add'}, m('i', {class: 'fa fa-plus'})),
m('div', {class: "btn-group btn-group-lg me-2"}, [
m('button', {type: "button", class: "btn btn-outline-secondary", onclick: function() { panel_show(data.filter.data) }},
m('i', {class: "fa fa-filter"})
),
m('button', {type: "button", class: "btn btn-outline-secondary", onclick: function() { panel_show(data.order_by.data) }},
m('i', {class: "fa fa-sort-alpha-asc"})
)
]),
'Мои заметки',
])
),
m('hr')
]);
result.push(m(data.filter));
result.push(m(data.order_by));
result.push(m(Pagination, data.pagination));
if (data.notes.length>0) {
result.push(m(ComponentNotes, {notes: data.notes}));
result.push(m(Pagination, data.pagination));
};
result.push(breadcrumbs_render());
return result;
}
}
};

View File

@@ -0,0 +1,130 @@
function NoteSource() {
let data = {
note: null,
editor: null,
}
function note_get(id) {
m.request({
url: '/api',
method: "POST",
body: {
"jsonrpc": "2.0",
"method": 'note',
"params": {
"id": id
},
"id": get_id()
}
}).then(
function(response) {
if ('result' in response) {
data.note = response['result'];
}
}
)
}
function note_update() {
data.note.body = data.editor.getValue();
m.request({
url: '/api',
method: "POST",
body: {
"jsonrpc": "2.0",
"method": 'note.update',
"params": {
"id": data.note.id,
"title": data.note.title,
"body": data.note.body
},
"id": get_id()
}
}).then(
function(response) {
if ('result' in response) {
m.route.set(`/note/${response['result'].id}`);
}
}
)
}
function editor_init() {
let vCode = document.getElementById('note_body');
const {EditorState} = CM["@codemirror/state"];
const {basicSetup, EditorView} = CM["codemirror"];
data.editor = {};
data.editor.state = EditorState.create({
doc: data.note.body,
extensions: [
basicSetup,
EditorView.updateListener.of(function(e) {
data.note.body = e.state.doc.toString();
}),
EditorView.lineWrapping
]
});
data.editor.view = new EditorView({
lineWrapping: true,
state: data.editor.state,
parent: vCode
});
};
function editor_remove() {
data.editor = null;
}
return {
oncreate(vnode) {
console.log(vnode);
if (data.note!=null) {
console.log(data.note);
editor_init();
}
},
onbeforeremove: function(vnode) {
editor_remove();
},
oninit: function(vnode) {
document.title = `Исходный код статьи - ${SETTINGS.TITLE}`;
note_get(vnode.attrs.id);
},
onupdate(vnode) {
console.log(vnode);
if (data.note!=null) {
if (data.editor==null) {
console.log(data.note);
editor_init();
}
}
},
view: function(vnode) {
let result = [];
if (data.note!=null) {
result.push([
m('div', {class: 'row'},
m('div', {class: 'col h1 py-1'}, [
m(m.route.Link, {class: "btn btn-outline-secondary btn-lg", href: `/note/${data.note.id}`, title: data.note.title}, m('i', {class: 'fa fa-chevron-left'})),
{tag: '<', children: '&nbsp;'},
'Исходный код статьи',
m('hr'),
])
),
m(NoteMenu, {menuitem: 'source', note: data.note}),
m('div', {class: 'row'},
m('div', {class: 'col py-2'}, [
m('button', {class: 'btn btn-outline-success float-end', type: 'button', onclick: note_update}, [m('i', {class: 'fa fa-save'}), ' Сохранить']),
])
),
m('div', {class: 'form-group mb-2'}, [
m('label', {class: 'form-label'}, 'Текст'),
m('div', {id: 'note_body', oninput: function (e) {data.note.body = e.target.value}, value: data.note.body})
]),
m('div', {class: 'row'},
m('div', {class: 'col py-2'}, [
m('button', {class: 'btn btn-outline-success float-end', type: 'button', onclick: note_update}, [m('i', {class: 'fa fa-save'}), ' Сохранить']),
])
),
]);
};
return result
}
}
}