tiny_admin 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE.txt +20 -0
- data/README.md +171 -0
- data/lib/tiny_admin/actions/basic_action.rb +17 -0
- data/lib/tiny_admin/actions/index.rb +48 -0
- data/lib/tiny_admin/actions/show.rb +23 -0
- data/lib/tiny_admin/authentication.rb +36 -0
- data/lib/tiny_admin/basic_app.rb +30 -0
- data/lib/tiny_admin/context.rb +9 -0
- data/lib/tiny_admin/field.rb +20 -0
- data/lib/tiny_admin/plugins/active_record_repository.rb +83 -0
- data/lib/tiny_admin/plugins/base_repository.rb +15 -0
- data/lib/tiny_admin/plugins/no_auth.rb +25 -0
- data/lib/tiny_admin/plugins/simple_auth.rb +44 -0
- data/lib/tiny_admin/router.rb +125 -0
- data/lib/tiny_admin/settings.rb +83 -0
- data/lib/tiny_admin/utils.rb +40 -0
- data/lib/tiny_admin/version.rb +5 -0
- data/lib/tiny_admin/views/actions/index.rb +106 -0
- data/lib/tiny_admin/views/actions/show.rb +57 -0
- data/lib/tiny_admin/views/components/filters_form.rb +57 -0
- data/lib/tiny_admin/views/components/flash.rb +25 -0
- data/lib/tiny_admin/views/components/head.rb +31 -0
- data/lib/tiny_admin/views/components/navbar.rb +42 -0
- data/lib/tiny_admin/views/components/pagination.rb +33 -0
- data/lib/tiny_admin/views/default_layout.rb +101 -0
- data/lib/tiny_admin/views/pages/page_not_found.rb +21 -0
- data/lib/tiny_admin/views/pages/record_not_found.rb +21 -0
- data/lib/tiny_admin/views/pages/root.rb +17 -0
- data/lib/tiny_admin/views/pages/simple_auth_login.rb +32 -0
- data/lib/tiny_admin.rb +26 -0
- metadata +118 -0
@@ -0,0 +1,83 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TinyAdmin
|
4
|
+
class Settings
|
5
|
+
include Singleton
|
6
|
+
include Utils
|
7
|
+
|
8
|
+
attr_accessor :authentication,
|
9
|
+
:components,
|
10
|
+
:extra_styles,
|
11
|
+
:navbar,
|
12
|
+
:page_not_found,
|
13
|
+
:record_not_found,
|
14
|
+
:repository,
|
15
|
+
:root,
|
16
|
+
:root_path,
|
17
|
+
:sections,
|
18
|
+
:scripts,
|
19
|
+
:style_links
|
20
|
+
|
21
|
+
attr_reader :pages, :resources
|
22
|
+
|
23
|
+
def load_settings
|
24
|
+
@authentication ||= {}
|
25
|
+
@authentication[:plugin] ||= Plugins::NoAuth
|
26
|
+
@authentication[:login] ||= Views::Pages::SimpleAuthLogin
|
27
|
+
@authentication[:plugin] = Object.const_get(authentication[:plugin]) if authentication[:plugin].is_a?(String)
|
28
|
+
|
29
|
+
@page_not_found ||= Views::Pages::PageNotFound
|
30
|
+
@page_not_found = Object.const_get(@page_not_found) if @page_not_found.is_a?(String)
|
31
|
+
@record_not_found ||= Views::Pages::RecordNotFound
|
32
|
+
@record_not_found = Object.const_get(@record_not_found) if @record_not_found.is_a?(String)
|
33
|
+
|
34
|
+
@pages ||= {}
|
35
|
+
@repository ||= Plugins::ActiveRecordRepository
|
36
|
+
@resources ||= {}
|
37
|
+
@root_path ||= '/admin'
|
38
|
+
@sections ||= []
|
39
|
+
|
40
|
+
@root ||= {}
|
41
|
+
@root[:title] ||= 'TinyAdmin'
|
42
|
+
@root[:path] ||= root_path
|
43
|
+
@root[:page] ||= Views::Pages::Root
|
44
|
+
|
45
|
+
if @authentication[:plugin] == Plugins::SimpleAuth
|
46
|
+
@authentication[:logout] ||= ['logout', "#{root_path}/auth/logout"]
|
47
|
+
end
|
48
|
+
|
49
|
+
@components ||= {}
|
50
|
+
@components[:flash] ||= Views::Components::Flash
|
51
|
+
@components[:head] ||= Views::Components::Head
|
52
|
+
@components[:navbar] ||= Views::Components::Navbar
|
53
|
+
@components[:pagination] ||= Views::Components::Pagination
|
54
|
+
|
55
|
+
@navbar = prepare_navbar(sections, root_path: root_path, logout: authentication[:logout])
|
56
|
+
end
|
57
|
+
|
58
|
+
def prepare_navbar(sections, root_path:, logout:)
|
59
|
+
items = sections.each_with_object({}) do |section, list|
|
60
|
+
slug = section[:slug]
|
61
|
+
case section[:type]&.to_sym
|
62
|
+
when :url
|
63
|
+
list[slug] = [section[:name], section[:url], section[:options]]
|
64
|
+
when :page
|
65
|
+
page = section[:page]
|
66
|
+
pages[slug] = page.is_a?(String) ? Object.const_get(page) : page
|
67
|
+
list[slug] = [section[:name], "#{root_path}/#{slug}"]
|
68
|
+
when :resource
|
69
|
+
repository = section[:repository] || settings.repository
|
70
|
+
resources[slug] = {
|
71
|
+
model: section[:model].is_a?(String) ? Object.const_get(section[:model]) : section[:model],
|
72
|
+
repository: repository.is_a?(String) ? Object.const_get(repository) : repository
|
73
|
+
}
|
74
|
+
resources[slug].merge! section.slice(:resource, :only, :index, :show, :collection_actions, :member_actions)
|
75
|
+
hidden = section[:options] && (section[:options].include?(:hidden) || section[:options].include?('hidden'))
|
76
|
+
list[slug] = [section[:name], "#{root_path}/#{slug}"] unless hidden
|
77
|
+
end
|
78
|
+
end
|
79
|
+
items['auth/logout'] = logout if logout
|
80
|
+
items
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TinyAdmin
|
4
|
+
module Utils
|
5
|
+
def params_to_s(params)
|
6
|
+
list = params.each_with_object([]) do |(param, value), result|
|
7
|
+
if value.is_a?(Hash)
|
8
|
+
result.concat(value.map { |k, v| "#{param}[#{k}]=#{v}" })
|
9
|
+
else
|
10
|
+
result.push(["#{param}=#{value}"])
|
11
|
+
end
|
12
|
+
end
|
13
|
+
list.join('&')
|
14
|
+
end
|
15
|
+
|
16
|
+
def prepare_page(page_class, title: nil, context: nil, query_string: '', options: [])
|
17
|
+
page_class.new.tap do |page|
|
18
|
+
page.setup_page(title: title, query_string: query_string, settings: settings)
|
19
|
+
page.setup_options(
|
20
|
+
context: context,
|
21
|
+
compact_layout: options.include?(:compact_layout),
|
22
|
+
no_menu: options.include?(:no_menu)
|
23
|
+
)
|
24
|
+
yield(page) if block_given?
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def route_for(section, reference: nil, action: nil)
|
29
|
+
[settings.root_path, section, reference, action].compact.join("/")
|
30
|
+
end
|
31
|
+
|
32
|
+
def context
|
33
|
+
TinyAdmin::Context.instance
|
34
|
+
end
|
35
|
+
|
36
|
+
def settings
|
37
|
+
TinyAdmin::Settings.instance
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TinyAdmin
|
4
|
+
module Views
|
5
|
+
module Actions
|
6
|
+
class Index < DefaultLayout
|
7
|
+
attr_reader :current_page, :fields, :pages, :prepare_record, :records
|
8
|
+
attr_accessor :actions, :filters
|
9
|
+
|
10
|
+
def setup_pagination(current_page:, pages:)
|
11
|
+
@current_page = current_page
|
12
|
+
@pages = pages
|
13
|
+
end
|
14
|
+
|
15
|
+
def setup_records(records:, fields:, prepare_record:)
|
16
|
+
@records = records
|
17
|
+
@fields = fields.index_by(&:name)
|
18
|
+
@prepare_record = prepare_record
|
19
|
+
end
|
20
|
+
|
21
|
+
def template
|
22
|
+
@filters ||= {}
|
23
|
+
|
24
|
+
super do
|
25
|
+
div(class: 'index') {
|
26
|
+
div(class: 'row') {
|
27
|
+
div(class: 'col-4') {
|
28
|
+
h1(class: 'title') { title }
|
29
|
+
}
|
30
|
+
div(class: 'col-8') {
|
31
|
+
ul(class: 'nav justify-content-end') {
|
32
|
+
(actions || []).each do |action|
|
33
|
+
li(class: 'nav-item') {
|
34
|
+
href = route_for(context.slug, action: action)
|
35
|
+
a(href: href, class: 'nav-link btn btn-outline-secondary') { action }
|
36
|
+
}
|
37
|
+
end
|
38
|
+
}
|
39
|
+
}
|
40
|
+
}
|
41
|
+
|
42
|
+
div(class: 'row') {
|
43
|
+
div_class = filters.any? ? 'col-9' : 'col-12'
|
44
|
+
div(class: div_class) {
|
45
|
+
table(class: 'table') {
|
46
|
+
table_header if fields.any?
|
47
|
+
|
48
|
+
table_body
|
49
|
+
}
|
50
|
+
}
|
51
|
+
|
52
|
+
if filters.any?
|
53
|
+
div(class: 'col-3') {
|
54
|
+
filters_form_attrs = { section_path: route_for(context.slug), filters: filters }
|
55
|
+
render TinyAdmin::Views::Components::FiltersForm.new(**filters_form_attrs)
|
56
|
+
}
|
57
|
+
end
|
58
|
+
}
|
59
|
+
|
60
|
+
if pages
|
61
|
+
render components[:pagination].new(current: current_page, pages: pages, query_string: query_string)
|
62
|
+
end
|
63
|
+
}
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
|
69
|
+
def table_header
|
70
|
+
thead {
|
71
|
+
tr {
|
72
|
+
fields.each_value do |field|
|
73
|
+
td(class: "field-header-#{field.name} field-header-type-#{field.type}") { field.title }
|
74
|
+
end
|
75
|
+
td { whitespace }
|
76
|
+
}
|
77
|
+
}
|
78
|
+
end
|
79
|
+
|
80
|
+
def table_body
|
81
|
+
tbody {
|
82
|
+
records.each_with_index do |record, index|
|
83
|
+
tr(class: "row_#{index + 1}") {
|
84
|
+
attributes = prepare_record.call(record)
|
85
|
+
attributes.each do |key, value|
|
86
|
+
field = fields[key]
|
87
|
+
td(class: "field-value-#{field.name} field-value-type-#{field.type}") {
|
88
|
+
if field.options && field.options[:link_to]
|
89
|
+
reference = record.send(field.options[:field])
|
90
|
+
a(href: route_for(field.options[:link_to], reference: reference)) { value }
|
91
|
+
else
|
92
|
+
value
|
93
|
+
end
|
94
|
+
}
|
95
|
+
end
|
96
|
+
td(class: 'actions') {
|
97
|
+
a(href: route_for(context.slug, reference: record.id)) { 'show' }
|
98
|
+
}
|
99
|
+
}
|
100
|
+
end
|
101
|
+
}
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TinyAdmin
|
4
|
+
module Views
|
5
|
+
module Actions
|
6
|
+
class Show < DefaultLayout
|
7
|
+
attr_reader :fields, :prepare_record, :record
|
8
|
+
attr_accessor :actions
|
9
|
+
|
10
|
+
def setup_record(record:, fields:, prepare_record:)
|
11
|
+
@record = record
|
12
|
+
@fields = fields
|
13
|
+
@prepare_record = prepare_record
|
14
|
+
end
|
15
|
+
|
16
|
+
def template
|
17
|
+
super do
|
18
|
+
div(class: 'show') {
|
19
|
+
div(class: 'row') {
|
20
|
+
div(class: 'col-4') {
|
21
|
+
h1(class: 'title') { title }
|
22
|
+
}
|
23
|
+
div(class: 'col-8') {
|
24
|
+
ul(class: 'nav justify-content-end') {
|
25
|
+
(actions || []).each do |action|
|
26
|
+
li(class: 'nav-item') {
|
27
|
+
href = route_for(context.slug, reference: context.reference, action: action)
|
28
|
+
a(href: href, class: 'nav-link btn btn-outline-secondary') { action }
|
29
|
+
}
|
30
|
+
end
|
31
|
+
}
|
32
|
+
}
|
33
|
+
}
|
34
|
+
|
35
|
+
prepare_record.call(record).each_with_index do |(_key, value), index|
|
36
|
+
field = fields[index]
|
37
|
+
div(class: "field-#{field.name} row lh-lg") {
|
38
|
+
if field
|
39
|
+
div(class: 'field-header col-2') { field.title }
|
40
|
+
end
|
41
|
+
div(class: 'field-value col-10') {
|
42
|
+
if field.options && field.options[:link_to]
|
43
|
+
reference = record.send(field.options[:field])
|
44
|
+
a(href: route_for(field.options[:link_to], reference: reference)) { value }
|
45
|
+
else
|
46
|
+
value
|
47
|
+
end
|
48
|
+
}
|
49
|
+
}
|
50
|
+
end
|
51
|
+
}
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TinyAdmin
|
4
|
+
module Views
|
5
|
+
module Components
|
6
|
+
class FiltersForm < Phlex::HTML
|
7
|
+
attr_reader :filters, :section_path
|
8
|
+
|
9
|
+
def initialize(section_path:, filters:)
|
10
|
+
@section_path = section_path
|
11
|
+
@filters = filters
|
12
|
+
end
|
13
|
+
|
14
|
+
def template
|
15
|
+
form(class: 'form_filters', method: 'get') {
|
16
|
+
filters.each do |field, filter|
|
17
|
+
name = field.name
|
18
|
+
filter_data = filter[:filter]
|
19
|
+
div(class: 'mb-3') {
|
20
|
+
label(for: "filter-#{name}", class: 'form-label') { field.title }
|
21
|
+
case filter_data[:type]&.to_sym || field.type
|
22
|
+
when :boolean
|
23
|
+
select(class: 'form-select', id: "filter-#{name}", name: "q[#{name}]") {
|
24
|
+
option(value: '') { '-' }
|
25
|
+
option(value: '0', selected: filter[:value] == '0') { 'false' }
|
26
|
+
option(value: '1', selected: filter[:value] == '1') { 'true' }
|
27
|
+
}
|
28
|
+
when :date
|
29
|
+
input(type: 'date', class: 'form-control', id: "filter-#{name}", name: "q[#{name}]", value: filter[:value])
|
30
|
+
when :datetime
|
31
|
+
input(type: 'datetime-local', class: 'form-control', id: "filter-#{name}", name: "q[#{name}]", value: filter[:value])
|
32
|
+
when :integer
|
33
|
+
input(type: 'number', class: 'form-control', id: "filter-#{name}", name: "q[#{name}]", value: filter[:value])
|
34
|
+
when :select
|
35
|
+
select(class: 'form-select', id: "filter-#{name}", name: "q[#{name}]") {
|
36
|
+
option(value: '') { '-' }
|
37
|
+
filter_data[:values].each do |value|
|
38
|
+
option(selected: filter[:value] == value) { value }
|
39
|
+
end
|
40
|
+
}
|
41
|
+
else
|
42
|
+
input(type: 'text', class: 'form-control', id: "filter-#{name}", name: "q[#{name}]", value: filter[:value])
|
43
|
+
end
|
44
|
+
}
|
45
|
+
end
|
46
|
+
|
47
|
+
div(class: 'mt-3') {
|
48
|
+
a(href: section_path, class: 'button_clear btn btn-secondary text-white') { 'clear' }
|
49
|
+
whitespace
|
50
|
+
button(type: 'submit', class: 'button_filter btn btn-secondary') { 'filter' }
|
51
|
+
}
|
52
|
+
}
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TinyAdmin
|
4
|
+
module Views
|
5
|
+
module Components
|
6
|
+
class Flash < Phlex::HTML
|
7
|
+
attr_reader :errors, :notices, :warnings
|
8
|
+
|
9
|
+
def initialize(notices: [], warnings: [], errors: [])
|
10
|
+
@notices = notices
|
11
|
+
@warnings = warnings
|
12
|
+
@errors = errors
|
13
|
+
end
|
14
|
+
|
15
|
+
def template
|
16
|
+
div(class: 'flash') {
|
17
|
+
div(class: 'notices alert alert-success', role: 'alert') { notices.join(', ') } if notices&.any?
|
18
|
+
div(class: 'notices alert alert-warning', role: 'alert') { warnings.join(', ') } if warnings&.any?
|
19
|
+
div(class: 'notices alert alert-danger', role: 'alert') { errors.join(', ') } if errors&.any?
|
20
|
+
}
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TinyAdmin
|
4
|
+
module Views
|
5
|
+
module Components
|
6
|
+
class Head < Phlex::HTML
|
7
|
+
attr_reader :extra_styles, :page_title, :style_links
|
8
|
+
|
9
|
+
def initialize(page_title, style_links: [], extra_styles: nil)
|
10
|
+
@page_title = page_title
|
11
|
+
@style_links = style_links
|
12
|
+
@extra_styles = extra_styles
|
13
|
+
end
|
14
|
+
|
15
|
+
def template
|
16
|
+
head {
|
17
|
+
meta charset: 'utf-8'
|
18
|
+
meta name: 'viewport', content: 'width=device-width, initial-scale=1'
|
19
|
+
title {
|
20
|
+
page_title
|
21
|
+
}
|
22
|
+
style_links.each do |style_link|
|
23
|
+
link(**style_link)
|
24
|
+
end
|
25
|
+
style { extra_styles } if extra_styles
|
26
|
+
}
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TinyAdmin
|
4
|
+
module Views
|
5
|
+
module Components
|
6
|
+
class Navbar < Phlex::HTML
|
7
|
+
attr_reader :current_slug, :items, :root
|
8
|
+
|
9
|
+
def initialize(root:, items: [], current_slug: nil)
|
10
|
+
@root = root
|
11
|
+
@items = items || []
|
12
|
+
@current_slug = current_slug
|
13
|
+
end
|
14
|
+
|
15
|
+
def template
|
16
|
+
nav(class: 'navbar navbar-expand-lg') {
|
17
|
+
div(class: 'container') {
|
18
|
+
a(class: 'navbar-brand', href: root[:path]) { root[:title] }
|
19
|
+
button(class: 'navbar-toggler', type: 'button', 'data-bs-toggle' => 'collapse', 'data-bs-target' => '#navbarNav', 'aria-controls' => 'navbarNav', 'aria-expanded' => 'false', 'aria-label' => 'Toggle navigation') {
|
20
|
+
span(class: 'navbar-toggler-icon')
|
21
|
+
}
|
22
|
+
div(class: 'collapse navbar-collapse', id: 'navbarNav') {
|
23
|
+
ul(class: 'navbar-nav') {
|
24
|
+
items.each do |slug, (name, path, options)|
|
25
|
+
classes = %w[nav-link]
|
26
|
+
classes << 'active' if slug == current_slug
|
27
|
+
link_attributes = { class: classes.join(' '), href: path, 'aria-current' => 'page' }
|
28
|
+
link_attributes.merge!(options) if options
|
29
|
+
|
30
|
+
li(class: 'nav-item') {
|
31
|
+
a(**link_attributes) { name }
|
32
|
+
}
|
33
|
+
end
|
34
|
+
}
|
35
|
+
}
|
36
|
+
}
|
37
|
+
}
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TinyAdmin
|
4
|
+
module Views
|
5
|
+
module Components
|
6
|
+
class Pagination < Phlex::HTML
|
7
|
+
attr_reader :current, :pages, :query_string
|
8
|
+
|
9
|
+
def initialize(current:, pages:, query_string:)
|
10
|
+
@current = current
|
11
|
+
@pages = pages
|
12
|
+
@query_string = query_string
|
13
|
+
end
|
14
|
+
|
15
|
+
def template
|
16
|
+
div(class: 'pagination-div') {
|
17
|
+
nav('aria-label' => 'Pagination') {
|
18
|
+
ul(class: 'pagination justify-content-center') {
|
19
|
+
1.upto(pages) do |i|
|
20
|
+
li_class = (i == current ? 'page-item active' : 'page-item')
|
21
|
+
li(class: li_class) {
|
22
|
+
href = query_string.empty? ? "?p=#{i}" : "?#{query_string}&p=#{i}"
|
23
|
+
a(class: 'page-link', href: href) { i }
|
24
|
+
}
|
25
|
+
end
|
26
|
+
}
|
27
|
+
}
|
28
|
+
}
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TinyAdmin
|
4
|
+
module Views
|
5
|
+
class DefaultLayout < Phlex::HTML
|
6
|
+
include Utils
|
7
|
+
|
8
|
+
attr_reader :compact_layout,
|
9
|
+
:context,
|
10
|
+
:errors,
|
11
|
+
:no_menu,
|
12
|
+
:notices,
|
13
|
+
:query_string,
|
14
|
+
:settings,
|
15
|
+
:title,
|
16
|
+
:warnings
|
17
|
+
|
18
|
+
def setup_page(title:, query_string:, settings:)
|
19
|
+
@title = title
|
20
|
+
@query_string = query_string
|
21
|
+
@settings = settings
|
22
|
+
end
|
23
|
+
|
24
|
+
def setup_options(context:, compact_layout:, no_menu:)
|
25
|
+
@context = context
|
26
|
+
@compact_layout = compact_layout
|
27
|
+
@no_menu = no_menu
|
28
|
+
end
|
29
|
+
|
30
|
+
def setup_flash_messages(notices: [], warnings: [], errors: [])
|
31
|
+
@notices = notices
|
32
|
+
@warnings = warnings
|
33
|
+
@errors = errors
|
34
|
+
end
|
35
|
+
|
36
|
+
def template(&block)
|
37
|
+
items = no_menu ? [] : settings.navbar
|
38
|
+
|
39
|
+
doctype
|
40
|
+
html {
|
41
|
+
render components[:head].new(title, style_links: style_links, extra_styles: settings.extra_styles)
|
42
|
+
|
43
|
+
body(class: body_class) {
|
44
|
+
render components[:navbar].new(current_slug: context&.slug, root: settings.root, items: items)
|
45
|
+
|
46
|
+
main_content {
|
47
|
+
render components[:flash].new(notices: notices, warnings: warnings, errors: errors)
|
48
|
+
yield_content(&block)
|
49
|
+
}
|
50
|
+
|
51
|
+
render_scripts
|
52
|
+
}
|
53
|
+
}
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
def components
|
59
|
+
settings.components
|
60
|
+
end
|
61
|
+
|
62
|
+
def style_links
|
63
|
+
settings.style_links || [
|
64
|
+
# Bootstrap CDN
|
65
|
+
{
|
66
|
+
href: 'https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css',
|
67
|
+
rel: 'stylesheet',
|
68
|
+
integrity: 'sha384-rbsA2VBKQhggwzxH7pPCaAqO46MgnOM80zW1RWuH61DGLwZJEdK2Kadq2F9CUG65',
|
69
|
+
crossorigin: 'anonymous'
|
70
|
+
}
|
71
|
+
]
|
72
|
+
end
|
73
|
+
|
74
|
+
def body_class
|
75
|
+
"module-#{self.class.to_s.split('::').last.downcase}"
|
76
|
+
end
|
77
|
+
|
78
|
+
def main_content
|
79
|
+
div(class: 'container main-content py-4') do
|
80
|
+
if compact_layout
|
81
|
+
div(class: 'row justify-content-center') {
|
82
|
+
div(class: 'col-6') {
|
83
|
+
yield
|
84
|
+
}
|
85
|
+
}
|
86
|
+
else
|
87
|
+
yield
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def render_scripts
|
93
|
+
return unless settings.scripts
|
94
|
+
|
95
|
+
settings.scripts.each do |script_attrs|
|
96
|
+
script(**script_attrs)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TinyAdmin
|
4
|
+
module Views
|
5
|
+
module Pages
|
6
|
+
class PageNotFound < DefaultLayout
|
7
|
+
def template
|
8
|
+
super do
|
9
|
+
div(class: 'page_not_found') {
|
10
|
+
h1(class: 'title') { title }
|
11
|
+
}
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def title
|
16
|
+
'Page not found'
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TinyAdmin
|
4
|
+
module Views
|
5
|
+
module Pages
|
6
|
+
class RecordNotFound < DefaultLayout
|
7
|
+
def template
|
8
|
+
super do
|
9
|
+
div(class: 'record_not_found') {
|
10
|
+
h1(class: 'title') { title }
|
11
|
+
}
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def title
|
16
|
+
'Record not found'
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TinyAdmin
|
4
|
+
module Views
|
5
|
+
module Pages
|
6
|
+
class SimpleAuthLogin < DefaultLayout
|
7
|
+
def template
|
8
|
+
super do
|
9
|
+
div(class: 'simple_auth_login') {
|
10
|
+
h1(class: 'title') { title }
|
11
|
+
|
12
|
+
form(class: 'form_login', method: 'post') {
|
13
|
+
div(class: 'mt-3') {
|
14
|
+
label(for: 'secret', class: 'form-label') { 'Password' }
|
15
|
+
input(type: 'password', name: 'secret', class: 'form-control', id: 'secret')
|
16
|
+
}
|
17
|
+
|
18
|
+
div(class: 'mt-3') {
|
19
|
+
button(type: 'submit', class: 'button_login btn btn-primary') { 'login' }
|
20
|
+
}
|
21
|
+
}
|
22
|
+
}
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def title
|
27
|
+
'Login'
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|