Skip to content

Commit 19dab98

Browse files
committed
feat(admin): add support for custom mobile tab items with the admin management UI
1 parent 4faf236 commit 19dab98

12 files changed

Lines changed: 332 additions & 10 deletions

composer.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@
2020
"flarum/core": "^2.0.0-beta",
2121
"php": "^8.2"
2222
},
23+
"autoload": {
24+
"psr-4": {
25+
"ACPL\\MobileTab\\": "src/"
26+
}
27+
},
2328
"authors": [
2429
{
2530
"name": "Rafał Całka",

extend.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace ACPL\MobileTab;
44

5+
use ACPL\MobileTab\Api\Resource\CustomTabItemResource;
56
use Flarum\Extend;
67
use Flarum\Frontend\Document;
78

@@ -30,4 +31,6 @@
3031
->content(function (Document $document) {
3132
$document->meta['viewport'] = "{$document->meta['viewport']}, viewport-fit=cover";
3233
}),
34+
35+
new Extend\ApiResource(CustomTabItemResource::class),
3336
];
Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,20 @@
1+
import app from 'flarum/admin/app';
12
import MobileTabItemsRegistry from '../common/MobileTabItemsRegistry';
3+
import CustomTabItem from './models/CustomTabItem';
24

35
export default class MobileTabItemsRegistryAdmin extends MobileTabItemsRegistry {
46
items() {
5-
return super.items();
7+
const items = super.items();
8+
9+
app.store.all<CustomTabItem>('custom-tab-items').forEach((item) => {
10+
items.add(`custom-${item.id()}`, {
11+
label: item.label(),
12+
icon: item.icon(),
13+
href: item.url(),
14+
source: 'user',
15+
});
16+
});
17+
18+
return items;
619
}
720
}
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import app from 'flarum/admin/app';
2+
import Button from 'flarum/common/components/Button';
3+
import Form from 'flarum/common/components/Form';
4+
import FormModal, { IFormModalAttrs } from 'flarum/common/components/FormModal';
5+
import Icon from 'flarum/common/components/Icon';
6+
import extractText from 'flarum/common/utils/extractText';
7+
import ItemList from 'flarum/common/utils/ItemList';
8+
import Stream from 'flarum/common/utils/Stream';
9+
import { Children, Vnode } from 'mithril';
10+
import CustomTabItem from '../models/CustomTabItem';
11+
12+
interface EditCustomTabItemModalAttrs extends IFormModalAttrs {
13+
model?: CustomTabItem;
14+
}
15+
16+
export default class EditCustomTabItemModal extends FormModal<EditCustomTabItemModalAttrs> {
17+
protected customTabItem!: CustomTabItem;
18+
protected label!: Stream<string>;
19+
protected icon!: Stream<string>;
20+
protected url!: Stream<string>;
21+
22+
oninit(vnode: Vnode<EditCustomTabItemModalAttrs, this>) {
23+
super.oninit(vnode);
24+
25+
this.customTabItem = this.attrs.model || app.store.createRecord('custom-tab-items');
26+
27+
this.label = Stream(this.customTabItem?.label() || '');
28+
this.icon = Stream(this.customTabItem?.icon() || '');
29+
this.url = Stream(this.customTabItem?.url() || '');
30+
}
31+
32+
className() {
33+
return 'EditCustomTabItemModal Modal--small';
34+
}
35+
36+
title() {
37+
const label = this.label();
38+
if (!label.length) return app.translator.trans('acpl-mobile-tab.admin.edit_item.modal_title');
39+
40+
const iconClass = this.icon();
41+
if (iconClass.length)
42+
return (
43+
<>
44+
<Icon name={iconClass} /> {label}
45+
</>
46+
);
47+
48+
return label;
49+
}
50+
51+
content() {
52+
return (
53+
<div className="Modal-body">
54+
<Form>{this.fields().toArray()}</Form>
55+
</div>
56+
);
57+
}
58+
59+
fields() {
60+
const items = new ItemList<Children>();
61+
62+
items.add(
63+
'label',
64+
<div className="Form-group">
65+
<label>{app.translator.trans('acpl-mobile-tab.admin.edit_item.label')}</label>
66+
<input
67+
className="FormControl"
68+
type="text"
69+
placeholder={app.translator.trans('acpl-mobile-tab.admin.edit_item.label_placeholder')}
70+
bidi={this.label}
71+
/>
72+
</div>
73+
);
74+
75+
items.add(
76+
'url',
77+
<div className="Form-group">
78+
<label>{app.translator.trans('acpl-mobile-tab.admin.edit_item.url')}</label>
79+
<input className="FormControl" type="url" placeholder="https://" bidi={this.url} />
80+
</div>
81+
);
82+
83+
items.add(
84+
'icon',
85+
<div className="Form-group">
86+
<label>{app.translator.trans('acpl-mobile-tab.admin.edit_item.icon')}</label>
87+
<input className="FormControl" type="text" placeholder="fas fa-home" bidi={this.icon} />
88+
</div>
89+
);
90+
91+
items.add(
92+
'actions',
93+
<div className="Form-group Form-controls">
94+
<Button type="submit" className="Button Button--primary">
95+
{app.translator.trans('acpl-mobile-tab.admin.edit_item.submit_button')}
96+
</Button>
97+
{this.customTabItem.exists && (
98+
<Button className="Button Button--danger EditCustomTabItemModal-delete" onclick={this.delete.bind(this)}>
99+
{app.translator.trans('acpl-mobile-tab.admin.edit_item.delete_button')}
100+
</Button>
101+
)}
102+
</div>
103+
);
104+
105+
return items;
106+
}
107+
108+
submitData() {
109+
return { label: this.label(), url: this.url(), icon: this.icon() };
110+
}
111+
112+
onsubmit(e: SubmitEvent) {
113+
e.preventDefault();
114+
this.loading = true;
115+
116+
this.customTabItem.save(this.submitData()).then(
117+
() => this.hide(),
118+
() => (this.loading = false)
119+
);
120+
}
121+
122+
delete() {
123+
if (!confirm(extractText(app.translator.trans('acpl-mobile-tab.admin.edit_item.delete_item_confirmation')))) {
124+
return;
125+
}
126+
127+
this.loading = true;
128+
this.customTabItem.delete().then(
129+
() => this.hide(),
130+
() => (this.loading = false)
131+
);
132+
}
133+
}

js/src/admin/components/MobileTabSettingsPage.tsx

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@ import FormSection from 'flarum/admin/components/FormSection';
44
import FormSectionGroup from 'flarum/admin/components/FormSectionGroup';
55
import Button from 'flarum/common/components/Button';
66
import ItemList from 'flarum/common/utils/ItemList';
7-
import { Children, VnodeDOM } from 'mithril';
7+
import { Children, Vnode } from 'mithril';
88
import Sortable from 'sortablejs';
99
import { MobileTabItemDefinition } from '../../common/types';
1010
import MobileTabItemsRegistryAdmin from '../MobileTabItemsRegistryAdmin';
11+
import CustomTabItem from '../models/CustomTabItem';
12+
import EditCustomTabItemModal from './EditCustomTabItemModal';
1113

1214
export default class MobileTabSettingsPage extends ExtensionPage {
1315
protected itemsSettingKey = 'acpl-mobile-tab.items';
@@ -20,6 +22,16 @@ export default class MobileTabSettingsPage extends ExtensionPage {
2022
*/
2123
protected forcedRefreshKey = 0;
2224

25+
oninit(vnode: Vnode<ExtensionPageAttrs, this>) {
26+
super.oninit(vnode);
27+
28+
this.loading = true;
29+
app.store.find<CustomTabItem>('custom-tab-items').then(() => {
30+
this.loading = false;
31+
m.redraw();
32+
});
33+
}
34+
2335
get activeKeys(): string[] {
2436
const raw = this.setting(this.itemsSettingKey)();
2537
if (Array.isArray(raw)) return raw;
@@ -35,7 +47,7 @@ export default class MobileTabSettingsPage extends ExtensionPage {
3547
this.setting(this.itemsSettingKey)(JSON.stringify(value));
3648
}
3749

38-
content(vnode: VnodeDOM<ExtensionPageAttrs, this>) {
50+
content() {
3951
return (
4052
<div className="ExtensionPage-settings">
4153
<div className="container" key={this.forcedRefreshKey} oncreate={this.onListCreate.bind(this)}>
@@ -57,12 +69,13 @@ export default class MobileTabSettingsPage extends ExtensionPage {
5769
.toArray()
5870
.map((item) => (
5971
<li className={`item-${item.itemName}`} key={item.itemName}>
60-
<Button className="Button MobileTab-item" icon={item.icon}>
61-
{item.label}
62-
</Button>
72+
{this.itemContent(item)}
6373
</li>
6474
))}
6575
</ul>
76+
<Button className="Button" icon="fas fa-plus" onclick={() => app.modal.show(EditCustomTabItemModal)}>
77+
{app.translator.trans('acpl-mobile-tab.admin.create_new_item_button')}
78+
</Button>
6679
</FormSection>
6780
);
6881
}
@@ -76,9 +89,7 @@ export default class MobileTabSettingsPage extends ExtensionPage {
7689
.toArray()
7790
.map((item) => (
7891
<li className={`item-${item.itemName}`} key={item.itemName}>
79-
<Button className="Button MobileTab-item" icon={item.icon}>
80-
{item.label}
81-
</Button>
92+
{this.itemContent(item)}
8293
</li>
8394
))}
8495
</ul>
@@ -87,6 +98,24 @@ export default class MobileTabSettingsPage extends ExtensionPage {
8798
);
8899
}
89100

101+
itemContent(item: ReturnType<ItemList<MobileTabItemDefinition>['toArray']>[number]): Children {
102+
return (
103+
<Button
104+
className="Button MobileTab-item"
105+
icon={item.icon}
106+
onclick={() => {
107+
if (item.source === 'user') {
108+
const id = item.itemName.match(/(\d+)$/);
109+
if (!id) return;
110+
app.modal.show(EditCustomTabItemModal, { model: app.store.getById('custom-tab-items', id[0]) });
111+
}
112+
}}
113+
>
114+
{item.label}
115+
</Button>
116+
);
117+
}
118+
90119
availableItems() {
91120
const registeredItems = new MobileTabItemsRegistryAdmin().items();
92121

js/src/admin/extend.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import Extend from 'flarum/common/extenders';
22
import MobileTabSettingsPage from './components/MobileTabSettingsPage';
3+
import CustomTabItem from './models/CustomTabItem';
34

4-
export default [new Extend.Admin().page(MobileTabSettingsPage)];
5+
export default [new Extend.Admin().page(MobileTabSettingsPage), new Extend.Store().add('custom-tab-items', CustomTabItem)];
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import Model from 'flarum/common/Model';
2+
3+
export default class CustomTabItem extends Model {
4+
label = Model.attribute<string>('label');
5+
url = Model.attribute<string>('url');
6+
icon = Model.attribute<string>('icon');
7+
createdAt = Model.attribute('createdAt', Model.transformDate);
8+
updatedAt = Model.attribute('updatedAt', Model.transformDate);
9+
}

less/admin.less

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,9 @@
2828
}
2929
}
3030
}
31+
32+
.EditCustomTabItemModal{
33+
&-delete{
34+
margin-left: auto;
35+
}
36+
}

locale/en.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,16 @@ acpl-mobile-tab:
22
admin:
33
available_items: "Available items"
44
active_items: "Active items"
5+
create_new_item_button: "Create new item"
6+
edit_item:
7+
modal_title: "Edit item"
8+
label: "Label"
9+
label_placeholder: "My custom item"
10+
url: "Url"
11+
icon: "Icon"
12+
submit_button: "Save"
13+
delete_button: "Delete"
14+
delete_item_confirmation: "Are you sure you want to delete this item?"
515
forum:
616

717
lib:
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
use Illuminate\Database\Schema\Blueprint;
4+
5+
use Flarum\Database\Migration;
6+
7+
return Migration::createTable(
8+
'custom_mobile_tab_items',
9+
function (Blueprint $table) {
10+
$table->increments('id');
11+
$table->string('label');
12+
$table->string('url');
13+
$table->string('icon');
14+
$table->boolean('is_new_tab')->default(false);
15+
$table->dateTime('created_at')->nullable()->useCurrent();
16+
$table->dateTime('updated_at')->nullable()->useCurrent()->useCurrentOnUpdate();
17+
}
18+
);
19+

0 commit comments

Comments
 (0)