def routes(self): return { f'{self._route_prefix}': m( app_container, { 'content': self._list(), 'header_content': self._header( '/', m( button, { 'content': 'New', 'left_icon': 'solid/plus', 'route': f'{self._route_prefix}/new' })), }), f'{self._route_prefix}/:id/view': self._detail(), f'{self._route_prefix}/new': m( app_container, { 'content': self._create(), 'header_content': self._header( self._route_prefix, # TODO onclick save the form m(button, { 'content': 'Save', 'left_icon': 'solid/save' })) }), }
def field_group(field_: str): t = get_type(field_) nested_form = issubclass(t, (ValueObject, Entity)) children = [] if config['labels']: children.append(label(field_)) if nested_form: children.append = [ m(Form(t(), { 'nested': True, 'required': is_required(field_) })) ] else: children.append(form_field(field_)) if nested_form is False: children.append(error_container(field_)) classes = '' if has_errors(field_): classes += '.error' if nested_form: return children else: return m(f'div.field-group{classes}', children)
def field_set(fields_: list, options: dict, legend: str): children = [] if legend is not None: children.append(m('legend', legend)) children.extend(list(map(field_group, fields_))) return m(f'fieldset', options, children)
def _header(self, return_route, right_button): def redirect(): m.route.set(return_route or '/') return [ m('div.w-8.invert-stroke.flex.flex-col.justify-center', m(Icon('solid/arrow-left', onclick=redirect))), right_button, ]
def crud(entity: str, cls, route_prefix: str, form_config: dict = None): if '.' not in entity: raise ff.LogicError('entity must be formatted as "<context>.<entity>"') parts = entity.split('.') context = parts[0] entity = parts[1] def crud_list(): entities = [] def set_entities(e): nonlocal entities entities = e bus.request(f'{context}.{inflection.pluralize(entity)}').then( set_entities) return { 'view': lambda: m('div', list(map(lambda e: e.to_dict(), entities))) } def crud_new(): return {'view': lambda: m(form(cls(), form_config))} def redirect(return_route): def _redirect(): m.route.set(return_route or '/') return _redirect add_route( route_prefix, compose(default_layout, crud_list, header=lambda: { 'view': lambda: [ m('div.w-8.invert-stroke.flex.flex-col.justify-center', m(Icon('solid/arrow-left', onclick=redirect('/')))), m( button({ 'content': 'New', 'left_icon': 'solid/plus', 'route': f'{route_prefix}/new' })) ] })) add_route(f'{route_prefix}/new', compose(default_layout, crud_new))
def datetime_inputs(f: str, classes: str, conf: dict): return [ m( f'input[type="date"][id="{f}_date"][name="{f}_date"]' f'[value="{moment(dto[f]).format("YYYY-MM-DD")}"]' f'.form-date-input.px-2.py-1.rounded-sm.h-12{classes}', conf, ), m( f'input[type="time"][id="{f}_date"][name="{f}_date"]' f'[value="{moment(dto[f]).format("HH:mm")}"]' f'.form-time-input.px-2.py-1.rounded-sm.h-12{classes}', conf, ), ]
def view(): config = {'onsubmit': handle_submit} if 'fields' in config: keys = config['fields'] else: keys = dto.keys() if 'exclude_fields' in config: keys = [x for x in keys if x not in config['exclude_fields']] if 'fieldsets' in config: form_fields = [] for fieldset in config['fieldsets']: form_fields.append( field_set( fieldset['fields'], fieldset['options'], fieldset['legend'] if 'legend' in fieldset else None)) else: form_fields = list(map(field_group, keys)) if 'nested' not in config or not config['nested']: form_fields.append( m('input[type="submit"][value="Submit"].hidden.md:block')) return _form(config, form_fields)
def text_input(f: str, classes: str, conf: dict, children, input_type: str = 'text'): return m( f'input[type="{input_type}"][id="{f}"][name="{f}"][value="{dto[f]}"][placeholder="{humanize(f)}"]' f'.form-text-input.px-2.py-1.rounded-sm{classes}.h-12', conf, children)
def view(): options = {} if onclick is not None: options['onclick'] = onclick item = m( 'div.ff-card.flex.flex-row.justify-start.h-16.md:justify-center.md:flex-col.md:w-56.md:ml-3.md:h-32', options, [ m( 'div.w-8.h-full.invert-stroke.ml-2.py-3.md:ml-0.md:h-20.md:w-full.md:flex.md:flex-row.md:justify-center', m(Icon(icon))), m( 'div.w-10/12.ml-3.flex.flex-col.justify-center.md:w-full.md:ml-0.md:flex-row.md:justify-center', text), ]) if route is not None: item = m(m.route.Link, {'href': route}, item) return item
def view(): config = {} if 'onclick' in data: config['onclick'] = data['onclick'] elif 'route' in data: def redirect(): m.route.set(data['route']) config['onclick'] = redirect content = [ m('span.flex.flex-col.justify-center.font-bold', data['content']) ] if 'left_icon' in data: content.insert( 0, m('div.invert-stroke.my-auto.w-5.mr-2', m(Icon(data['left_icon'])))) if 'right_icon' in data: content.append( m('div.invert-stroke.my-auto.w-5.ml-2', m(Icon(data['right_icon'])))) classes = '' if style == 'horizontal': classes += '.flex.flex-row.justify-between' return m( f'button[type="button"]{classes}.h-10.border.rounded.px-2.my-auto', config, content)
def select(f: str, options, classes: str, conf: dict): value = dto[f] options = list(options) options.insert(0, humanize(f)) def option(f): selected = '' if f == value: selected = '[selected="selected"]' return m(f'option[value="{f}"]{selected}', f) return m(f'select.form-select{classes}.h-12', conf, list(map(option, options)))
def crud_list(): entities = [] def set_entities(e): nonlocal entities entities = e bus.request(f'{context}.{inflection.pluralize(entity)}').then( set_entities) return { 'view': lambda: m('div', list(map(lambda e: e.to_dict(), entities))) }
def _list(self): context = self._context entity = self._entity entities = [] class ListEntity: @staticmethod def oninit(): def set_entities(e): nonlocal entities entities = e m.redraw() # TODO Use a QueryStream bus.request(f'{context}.{inflection.pluralize(entity)}').then( set_entities) @staticmethod def view(): return m('div', list(map(lambda e: e.to_dict(), entities))) return m(ListEntity())
def error_container(f: str, conf=None): return m(f'span[id="{f}_error"].errors', conf, error_messages(f))
def label(f: str, conf=None): return f'label', m(f'label[for="{f}"].block', conf, humanize(f))
def _form(conf=None, children=None): if 'nested' in config and config['nested']: return children return m('div.form-container.flex.flex-col', m(f'form#{config["id"]}', conf, children))
def crud_new(): return {'view': lambda: m(form(cls(), form_config))}
def option(f): selected = '' if f == value: selected = '[selected="selected"]' return m(f'option[value="{f}"]{selected}', f)
def _create(self): return m(Form(self._class(), self._form_config))
def boolean_input(f: str, classes: str, conf: dict): if dto[f]: conf['checked'] = 'checked' return m( f'input[type="checkbox"][id="{f}"][name="{f}"].form-checkbox-input{classes}', conf)
def error_messages(f: str): if f not in errors or not has_value(f): return [] return m('ul', map(lambda msg: m('li', msg), errors[f]))
def default_layout(main, menu, header, drawer, footer): drawer_is_open = False def close_drawer(): nonlocal drawer_is_open drawer_is_open = False def open_drawer(): nonlocal drawer_is_open drawer_is_open = True return lambda: [ m('div.app.flex.flex-col', [ m( 'div.ff-header.fixed.flex.flex-row.justify-between.h-20.w-full.z-30.px-5.md:ml-20.md:pr-20', [ m('div.my-auto', m(firefly_logo)) if header is None else m(header), ]), m('div.flex.flex-row', [ m( 'div.ff-menu.fixed.z-30.w-full.h-20.bottom-0.flex.flex-row.justify-between' '.md:left-0.md:bottom-auto.md:flex-col.md:justify-start.md:w-20.md:h-full', menu or ''), m( 'div.ff-content.z-0.w-full.mb-20.mt-20.flex.flex-col.justify-between' '.md:ml-20.md:mb-0.md:flex-row.md:flex-wrap.md:justify-start', m(main) if callable(main) or hasattr(main, 'view') else main) ]), m('div.ff-footer', footer) if footer is not None else '', ]), m( f'div.ff-drawer-bg.z-40{".open" if drawer_is_open is True else ""}', {'onclick': close_drawer}, m( 'div.ff-drawer', {'onclick': lambda e: e.stopPropagation()}, m('div.flex.flex-row.h-full', [ m('div.w-10/12', drawer or ''), m( 'div.w-2/12.h-full.px-1.flex.flex-col.justify-center', m('div.invert-stroke.w-full.h-10', { 'onclick': close_drawer }, m(Icon('solid/chevron-left')))) ]))) ]
def view(): return m('div', list(map(lambda e: e.to_dict(), entities)))
# Firefly is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the # implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General # Public License for more details. You should have received a copy of the GNU Lesser General Public # License along with this program. If not, see <http://www.gnu.org/licenses/>. # # You should have received a copy of the GNU General Public License along with Firefly. If not, see # <http://www.gnu.org/licenses/>. from firefly.presentation.web.js_libs.mithril import m from firefly.presentation.web.polyfills import * # __:skip firefly_icon = { 'view': lambda: m('div.logo-image.rounded-full', [ m('div.firefly.one'), m('div.firefly.two'), m('div.firefly.three'), ]) } firefly_logo_text = { 'view': lambda: [ m('span.fire.inline-block.ml-2', 'Fire'), m('span.fly.inline-block', 'fly') ] } firefly_logo = { 'view': lambda: m('div.logo.font-sans.font-bold.text-shadow.rambla.flex.flex-row.justify-start', [
# implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General # Public License for more details. You should have received a copy of the GNU Lesser General Public # License along with this program. If not, see <http://www.gnu.org/licenses/>. # # You should have received a copy of the GNU General Public License along with Firefly. If not, see # <http://www.gnu.org/licenses/>. from firefly.presentation.web.components.layouts.default import menu_item, compose, default_layout from firefly.presentation.web.js_libs.mithril import m from firefly.presentation.web.plugins import add_route, add_menu_item from firefly.presentation.web.polyfills import * # __:skip # __pragma__('opov') m.route.prefix = '/admin' add_menu_item(m('div.ff-title', 'Kernel'), 0) add_menu_item(m(menu_item('Configuration', icon='solid/cog')), 1) add_menu_item(m(menu_item('System Health', icon='solid/heart')), 2) add_menu_item(m(menu_item('Services', icon='solid/wifi')), 3) add_route('/', compose(default_layout, lambda: window.ff_menu)) m.route(document.body, '/', window.ff_routes) """ __pragma__('js', '{}', ''' if (module.hot) { module.hot.accept(); } ''') """