1
0
mirror of https://github.com/flarum/core.git synced 2025-08-20 23:31:27 +02:00

feat: extension list UI (#4066)

This commit is contained in:
Sami Mazouz
2024-10-16 18:12:46 +01:00
committed by GitHub
parent b0e8f5ca36
commit 0107c96fb7
59 changed files with 1769 additions and 514 deletions

View File

@@ -15,12 +15,7 @@ export default class ControlSection extends Component<ComponentAttrs> {
view() {
return (
<div className="ExtensionPage-permissions ExtensionManager-controlSection">
<div className="ExtensionPage-permissions-header">
<div className="container">
<h2 className="ExtensionTitle">{app.translator.trans('flarum-extension-manager.admin.sections.control.title')}</h2>
</div>
</div>
<div className="ExtensionPage-settings ExtensionManager-controlSection">
<div className="container">
{app.data['flarum-extension-manager.writable_dirs'] ? (
<Form>

View File

@@ -0,0 +1,298 @@
import app from 'flarum/admin/app';
import Component, { type ComponentAttrs } from 'flarum/common/Component';
import Form from 'flarum/common/components/Form';
import Button from 'flarum/common/components/Button';
import type Mithril from 'mithril';
import LoadingIndicator from 'flarum/common/components/LoadingIndicator';
import ItemList from 'flarum/common/utils/ItemList';
import Input from 'flarum/common/components/Input';
import Stream from 'flarum/common/utils/Stream';
import Alert from 'flarum/common/components/Alert';
import listItems from 'flarum/common/helpers/listItems';
import LinkButton from 'flarum/common/components/LinkButton';
import Dropdown from 'flarum/common/components/Dropdown';
import type ExternalExtension from '../models/ExternalExtension';
import ExtensionCard from './ExtensionCard';
import Pagination from 'flarum/common/components/Pagination';
import InfoTile from 'flarum/common/components/InfoTile';
import classList from 'flarum/common/utils/classList';
import { throttle } from 'flarum/common/utils/throttleDebounce';
export interface IDiscoverSectionAttrs extends ComponentAttrs {}
export default class DiscoverSection<CustomAttrs extends IDiscoverSectionAttrs = IDiscoverSectionAttrs> extends Component<CustomAttrs> {
protected search = Stream('');
protected warningsDismissed = Stream(false);
oninit(vnode: Mithril.Vnode<CustomAttrs, this>) {
super.oninit(vnode);
app.extensionManager.extensions.goto(1);
this.warningsDismissed(localStorage.getItem('flarum-extension-manager.warningsDismissed') === 'true');
}
load(page = 1) {
app.extensionManager.extensions.goto(page);
}
view() {
return (
<div className="ExtensionPage-settings ExtensionManager-DiscoverSection">
<div className="container">
<Form>
<div className="Form-group">
<label>{app.translator.trans('flarum-extension-manager.admin.sections.discover.title')}</label>
<div className="helpText">
{app.translator.trans('flarum-extension-manager.admin.sections.discover.description')}
{this.warningsDismissed() && (
<Button
className="Button Button--text Button--warning Button--more"
icon="fas fa-exclamation-triangle"
onclick={() => this.setWarningDismissed(false)}
/>
)}
</div>
</div>
{!this.warningsDismissed() && (
<div className="ExtensionManager-warnings Form-group">
<Alert className="ExtensionManager-primaryWarning" type="warning" dismissible={true} ondismiss={() => this.setWarningDismissed(true)}>
<ul>{listItems(this.warningItems().toArray())}</ul>
</Alert>
</div>
)}
<div className="Tabs">
<div className="Tabs-nav">{this.tabItems().toArray()}</div>
<div className="Tabs-content">
<hr className="Tabs-divider" />
<div className="ExtensionManager-DiscoverSection-toolbar">
<div className="ExtensionManager-DiscoverSection-toolbar-primary">{this.toolbarPrimaryItems().toArray()}</div>
<div className="ExtensionManager-DiscoverSection-toolbar-secondary">{this.toolbarSecondaryItems().toArray()}</div>
</div>
{this.extensionList()}
<div className="ExtensionManager-DiscoverSection-footer">{this.footerItems().toArray()}</div>
</div>
</div>
</Form>
</div>
</div>
);
}
tabFilters(): Record<string, { label: Mithril.Children; active: () => boolean }> {
return {
'': {
label: app.translator.trans('flarum-extension-manager.admin.sections.discover.tabs.discover'),
active: () => !app.extensionManager.extensions.getParams().filter?.type,
},
extension: {
label: app.translator.trans('flarum-extension-manager.admin.sections.discover.tabs.extensions'),
active: () => app.extensionManager.extensions.getParams().filter?.type === 'extension',
},
locale: {
label: app.translator.trans('flarum-extension-manager.admin.sections.discover.tabs.languages'),
active: () => app.extensionManager.extensions.getParams().filter?.type === 'locale',
},
theme: {
label: app.translator.trans('flarum-extension-manager.admin.sections.discover.tabs.themes'),
active: () => app.extensionManager.extensions.getParams().filter?.type === 'theme',
},
};
}
tabItems() {
const items = new ItemList();
const tabs = this.tabFilters();
Object.keys(tabs).forEach((key) => {
const tab = tabs[key];
items.add(
key,
<Button
className="Button Button--link"
active={tab.active()}
onclick={() => {
app.extensionManager.extensions.changeFilter('type', key);
}}
>
{tab.label}
</Button>
);
});
return items;
}
warningItems() {
const items = new ItemList<Mithril.Children>();
items.add('accessWarning', app.translator.trans('flarum-extension-manager.admin.settings.access_warning'));
if (app.data.debugEnabled) {
items.add('devModeWarning', app.translator.trans('flarum-extension-manager.admin.settings.debug_mode_warning'));
}
return items;
}
private applySearch = throttle(1200, (value: string) => {
const params = app.extensionManager.extensions.getParams();
app.extensionManager.extensions.refreshParams({ ...params, filter: { ...params.filter, q: value } }, 1);
});
toolbarPrimaryItems() {
const items = new ItemList();
items.add(
'search',
<Input
value={this.search()}
onchange={(value: string) => {
this.search(value);
this.applySearch(value);
}}
inputAttrs={{ className: 'FormControl-alt' }}
clearable={true}
placeholder={app.translator.trans('flarum-extension-manager.admin.sections.discover.search')}
prefixIcon="fas fa-search"
/>
);
return items;
}
toolbarSecondaryItems() {
const items = new ItemList();
const sortMap = app.extensionManager.extensions.sortMap();
const sortOptions = Object.keys(sortMap).reduce((acc: any, sortId) => {
const sort = sortMap[sortId];
acc[sortId] = typeof sort !== 'string' ? sort.label : sort;
return acc;
}, {});
items.add(
'sort',
<Dropdown
buttonClassName="Button"
label={sortOptions[app.extensionManager.extensions.getParams().sort] || Object.keys(sortMap).map((key) => sortOptions[key])[0]}
accessibleToggleLabel={app.translator.trans('flarum-extension-manager.admin.sections.discover.sort.toggle_dropdown_accessible_label')}
>
{Object.keys(sortOptions).map((value) => {
const label = sortOptions[value];
const active = app.extensionManager.extensions.getParams().sort === value;
return (
<Button icon={active ? 'fas fa-check' : true} onclick={() => app.extensionManager.extensions.changeSort(value)} active={active}>
{label}
</Button>
);
})}
</Dropdown>
);
const is = app.extensionManager.extensions.getParams().filter?.is?.[0] ?? null;
const activeType = is || 'all';
items.add(
'party',
<Dropdown
buttonClassName="Button"
label={app.translator.trans('flarum-extension-manager.admin.sections.discover.party_filter.' + activeType)}
accessibleToggleLabel={app.translator.trans('flarum-extension-manager.admin.sections.discover.party_filter.toggle_dropdown_accessible_label')}
>
{['all', 'premium'].map((party) => (
<Button
icon={activeType === party ? 'fas fa-check' : true}
onclick={() => {
app.extensionManager.extensions.changeFilter('is', party === 'all' ? undefined : [party]);
}}
active={activeType === party}
>
{app.translator.trans('flarum-extension-manager.admin.sections.discover.party_filter.' + party)}
</Button>
))}
</Dropdown>
);
return items;
}
extensionList() {
if (!app.extensionManager.extensions.hasItems() && app.extensionManager.extensions.isLoading()) {
return <LoadingIndicator display="block" />;
}
if (!app.extensionManager.extensions.hasItems()) {
return (
<div className="ExtensionManager-DiscoverSection-list ExtensionManager-DiscoverSection-list--empty">
<InfoTile icon="fas fa-plug-circle-exclamation">
{app.translator.trans('flarum-extension-manager.admin.sections.discover.empty_results')}
</InfoTile>
</div>
);
}
return (
<div
className={classList('ExtensionManager-DiscoverSection-list', {
'loading-container': app.extensionManager.extensions.isLoading(),
})}
>
<div className="ExtensionManager-DiscoverSection-list-inner">
{app.extensionManager.extensions
.getPages()
.map((page) => page.items.map((extension: ExternalExtension) => <ExtensionCard extension={extension} key={extension.name()} />))}
</div>
{app.extensionManager.extensions.hasItems() && app.extensionManager.extensions.isLoading() && <LoadingIndicator size="large" />}
</div>
);
}
footerItems() {
const items = new ItemList<Mithril.Children>();
items.add(
'pagination',
<Pagination
total={app.extensionManager.extensions.totalItems}
perPage={app.extensionManager.extensions.pageSize}
currentPage={app.extensionManager.extensions.getLocation().page}
onChange={(page: number) => {
const current = app.extensionManager.extensions.getLocation().page;
if (current === page) {
return;
}
this.load(page);
}}
/>
);
items.add(
'premiumTermsLink',
<LinkButton
className="Button Button--link"
href="https://flarum.org/terms/premium-extensions"
external={true}
target="_blank"
icon="fas fa-circle-info"
>
{app.translator.trans('flarum-extension-manager.admin.sections.discover.premium_extension_terms')}
</LinkButton>
);
return items;
}
private setWarningDismissed(dismissed: boolean) {
this.warningsDismissed(dismissed);
localStorage.setItem('flarum-extension-manager.warningsDismissed', dismissed ? 'true' : 'false');
}
}

View File

@@ -0,0 +1,292 @@
import Component, { type ComponentAttrs } from 'flarum/common/Component';
import Icon from 'flarum/common/components/Icon';
import Badge from 'flarum/common/components/Badge';
import app from 'flarum/admin/app';
import Button from 'flarum/common/components/Button';
import formatAmount from 'flarum/common/utils/formatAmount';
import { type Extension as ExtensionInfo } from 'flarum/admin/AdminApplication';
import ExternalExtension from '../models/ExternalExtension';
import { UpdatedPackage } from '../states/ControlSectionState';
import ItemList from 'flarum/common/utils/ItemList';
import type Mithril from 'mithril';
import classList from 'flarum/common/utils/classList';
import Label from './Label';
import Tooltip from 'flarum/common/components/Tooltip';
import Dropdown from 'flarum/common/components/Dropdown';
import WhyNotModal from './WhyNotModal';
import LinkButton from 'flarum/common/components/LinkButton';
export type CommonExtension = ExternalExtension | ExtensionInfo;
export interface IExtensionAttrs extends ComponentAttrs {
extension: CommonExtension;
updates?: UpdatedPackage;
onClickUpdate?:
| CallableFunction
| {
soft: CallableFunction;
hard: CallableFunction;
};
whyNotWarning?: boolean;
isCore?: boolean;
updatable?: boolean;
isDanger?: boolean;
}
export default class ExtensionCard<CustomAttrs extends IExtensionAttrs = IExtensionAttrs> extends Component<CustomAttrs> {
getExtension() {
return this.attrs.extension instanceof ExternalExtension ? this.attrs.extension.toLocalExtension() : this.attrs.extension;
}
view() {
const extension = this.getExtension();
const { isCore, isDanger } = this.attrs;
return (
<div
className={classList('ExtensionCard', {
'ExtensionCard--core': isCore,
'ExtensionCard--danger': isDanger,
})}
>
<div className="ExtensionCard-header">
{this.icon()}
<Tooltip text={extension.name}>
<h4>{extension.extra['flarum-extension'].title}</h4>
</Tooltip>
{this.attrs.extension instanceof ExternalExtension && <div className="ExtensionCard-badges">{this.badges().toArray()}</div>}
<div className="ExtensionCard-actions">{this.actionItems().toArray()}</div>
</div>
<div className="ExtensionCard-body">
<p>{extension.description}</p>
</div>
<div className="ExtensionCard-footer">
<div className="ExtensionCard-meta">{this.metaItems().toArray()}</div>
</div>
</div>
);
}
icon() {
const extension = this.getExtension();
if (this.attrs.extension instanceof ExternalExtension && extension.id in app.data.extensions) {
extension.icon = app.data.extensions[extension.id].icon;
}
const style: any = extension.icon || {};
if (
!extension.icon?.name &&
this.attrs.extension instanceof ExternalExtension &&
!(extension.id in app.data.extensions) &&
this.attrs.extension.iconUrl()
) {
style.backgroundImage = `url(${this.attrs.extension.iconUrl()})`;
}
return (
<span className="ExtensionIcon" style={extension.icon}>
{extension.icon?.name ? <Icon name={extension.icon.name} /> : null}
</span>
);
}
badges() {
const items = new ItemList<Mithril.Children>();
const extension = this.attrs.extension as ExternalExtension;
if (extension.isSupported()) {
items.add(
'compatible',
<Badge
icon="fas fa-check"
type="success"
label={app.translator.trans('flarum-extension-manager.admin.sections.discover.extension.badges.compatible')}
className="Badge--flat Badge--square"
/>
);
} else {
items.add(
'incompatible',
<Badge
icon="fas fa-times"
type="danger"
label={app.translator.trans('flarum-extension-manager.admin.sections.discover.extension.badges.incompatible')}
className="Badge--flat Badge--square"
/>
);
}
if (extension.isPremium()) {
items.add(
'premium',
<Badge
icon="fas fa-dollar-sign"
label={app.translator.trans('flarum-extension-manager.admin.sections.discover.extension.badges.premium')}
className="ExtensionCard-badge--premium Badge--flat Badge--square"
/>
);
}
if (!extension.isStable()) {
items.add(
'unstable',
<Badge
icon="fas fa-flask"
label={app.translator.trans('flarum-extension-manager.admin.sections.discover.extension.badges.unstable')}
className="Badge--flat Badge--square Badge--danger"
/>
);
}
if (extension.name().split('/')[0] === 'fof') {
items.add(
'fof',
<Badge
icon="fas fa-users"
label={app.translator.trans('flarum-extension-manager.admin.sections.discover.extension.badges.fof')}
className="Badge--flat Badge--square"
/>
);
}
if (extension.name().split('/')[0] === 'flarum') {
items.add(
'flarum',
<Badge
icon="fab fa-flarum"
label={app.translator.trans('flarum-extension-manager.admin.sections.discover.extension.badges.flarum')}
className="ExtensionCard-badge--flarum Badge--flat Badge--square"
/>
);
}
return items;
}
metaItems() {
const items = new ItemList<Mithril.Children>();
const { updates, isCore } = this.attrs;
const latestVersion = updates ? updates['latest-minor'] ?? (updates['latest-major'] && !isCore ? updates['latest-major'] : null) : null;
if (this.attrs.extension instanceof ExternalExtension) {
items.add(
'downloads',
<span>
<Icon name="fas fa-circle-down" />
{app.translator.trans('flarum-extension-manager.admin.sections.discover.extension.downloads', {
count: this.attrs.extension.downloads(),
formattedCount: formatAmount(this.attrs.extension.downloads()),
})}
</span>
);
} else {
items.add(
'version',
<div className="ExtensionCard-version">
<span className="ExtensionCard-version-current">{this.version(updates!['version'])}</span>
{latestVersion ? (
<>
<Icon name="fas fa-arrow-right" />
<Label className="ExtensionCard-version-latest" type={updates!['latest-minor'] ? 'success' : 'warning'}>
{this.version(latestVersion)}
</Label>
</>
) : null}
</div>
);
}
if (this.attrs.extension instanceof ExternalExtension) {
items.add('version', <div className="ExtensionCard-version">v{this.version(this.attrs.extension.highestVersion())}</div>);
items.add(
'link',
<LinkButton
className="Button Button--ua-reset Button--link Button--icon"
href={this.attrs.extension.httpUri()}
target="_blank"
icon="fas fa-external-link-alt"
external={true}
/>
);
}
return items;
}
actionItems() {
const items = new ItemList<Mithril.Children>();
const { updates, extension, onClickUpdate, whyNotWarning } = this.attrs;
if (extension instanceof ExternalExtension) {
if (!(extension.extensionId() in app.data.extensions)) {
items.add(
'install',
<Button
className="Button Button--icon Button--flat"
icon="fas fa-cloud-arrow-down"
onclick={() => {
app.extensionManager.control.requirePackage({ package: extension.name() });
}}
/>
);
} else {
items.add('installed', <Button className="Button Button--icon Button--flat Button--success" icon="fas fa-check-circle" disabled={true} />);
}
} else {
if (onClickUpdate && typeof onClickUpdate === 'function') {
items.add(
'update',
<Tooltip text={app.translator.trans('flarum-extension-manager.admin.extensions.update')}>
<Button
icon="fas fa-cloud-arrow-down"
className="Button Button--icon Button--flat"
onclick={onClickUpdate}
aria-label={app.translator.trans('flarum-extension-manager.admin.extensions.update')}
/>
</Tooltip>
);
} else if (onClickUpdate) {
items.add(
'update',
<Dropdown
buttonClassName="Button Button--icon Button--flat"
icon="fas fa-ellipsis"
label={app.translator.trans('flarum-extension-manager.admin.extensions.update')}
>
<Button icon="fas fa-cloud-arrow-down" onclick={onClickUpdate.soft}>
{app.translator.trans('flarum-extension-manager.admin.extensions.update_soft_label')}
</Button>
<Button icon="fas fa-rotate" onclick={onClickUpdate.hard} disabled={!updates!['direct-dependency']}>
{app.translator.trans('flarum-extension-manager.admin.extensions.update_hard_label')}
</Button>
</Dropdown>
);
}
if (whyNotWarning)
items.add(
'whyNot',
<Tooltip text={app.translator.trans('flarum-extension-manager.admin.extensions.check_why_it_failed_updating')}>
<Button
icon="fas fa-exclamation-circle"
className="Button Button--icon Button--flat Button--danger"
onclick={() => app.modal.show(WhyNotModal, { package: extension.name })}
aria-label={app.translator.trans('flarum-extension-manager.admin.extensions.check_why_it_failed_updating')}
/>
</Tooltip>
);
}
return items;
}
version(v: string): string {
return v.charAt(0) === 'v' ? v.substring(1) : v;
}
}

View File

@@ -1,99 +0,0 @@
import type Mithril from 'mithril';
import app from 'flarum/admin/app';
import Component, { ComponentAttrs } from 'flarum/common/Component';
import classList from 'flarum/common/utils/classList';
import Icon from 'flarum/common/components/Icon';
import Tooltip from 'flarum/common/components/Tooltip';
import Button from 'flarum/common/components/Button';
import { Extension } from 'flarum/admin/AdminApplication';
import { UpdatedPackage } from '../states/ControlSectionState';
import WhyNotModal from './WhyNotModal';
import Label from './Label';
import Dropdown from 'flarum/common/components/Dropdown';
export interface ExtensionItemAttrs extends ComponentAttrs {
extension: Extension;
updates: UpdatedPackage;
onClickUpdate:
| CallableFunction
| {
soft: CallableFunction;
hard: CallableFunction;
};
whyNotWarning?: boolean;
isCore?: boolean;
updatable?: boolean;
isDanger?: boolean;
}
export default class ExtensionItem<Attrs extends ExtensionItemAttrs = ExtensionItemAttrs> extends Component<Attrs> {
view(vnode: Mithril.Vnode<Attrs, this>): Mithril.Children {
const { extension, updates, onClickUpdate, whyNotWarning, isCore, isDanger } = this.attrs;
const latestVersion = updates['latest-minor'] ?? (updates['latest-major'] && !isCore ? updates['latest-major'] : null);
return (
<div
className={classList({
'ExtensionManager-extension': true,
'ExtensionManager-extension--core': isCore,
'ExtensionManager-extension--danger': isDanger,
})}
>
<div className="ExtensionManager-extension-icon ExtensionIcon" style={extension.icon}>
{extension.icon ? <Icon name={extension.icon.name} /> : ''}
</div>
<div className="ExtensionManager-extension-info">
<div className="ExtensionManager-extension-name">{extension.extra['flarum-extension'].title}</div>
<div className="ExtensionManager-extension-version">
<span className="ExtensionManager-extension-version-current">{this.version(updates['version'])}</span>
{latestVersion ? (
<Label className="ExtensionManager-extension-version-latest" type={updates['latest-minor'] ? 'success' : 'warning'}>
{this.version(latestVersion)}
</Label>
) : null}
</div>
</div>
<div className="ExtensionManager-extension-controls">
{onClickUpdate && typeof onClickUpdate === 'function' ? (
<Tooltip text={app.translator.trans('flarum-extension-manager.admin.extensions.update')}>
<Button
icon="fas fa-arrow-alt-circle-up"
className="Button Button--icon Button--flat"
onclick={onClickUpdate}
aria-label={app.translator.trans('flarum-extension-manager.admin.extensions.update')}
/>
</Tooltip>
) : onClickUpdate ? (
<Dropdown
buttonClassName="Button Button--icon Button--flat"
icon="fas fa-arrow-alt-circle-up"
label={app.translator.trans('flarum-extension-manager.admin.extensions.update')}
>
<Button icon="fas fa-arrow-alt-circle-up" className="Button" onclick={onClickUpdate.soft}>
{app.translator.trans('flarum-extension-manager.admin.extensions.update_soft_label')}
</Button>
<Button icon="fas fa-arrow-alt-circle-up" className="Button" onclick={onClickUpdate.hard} disabled={!updates['direct-dependency']}>
{app.translator.trans('flarum-extension-manager.admin.extensions.update_hard_label')}
</Button>
</Dropdown>
) : null}
{whyNotWarning ? (
<Tooltip text={app.translator.trans('flarum-extension-manager.admin.extensions.check_why_it_failed_updating')}>
<Button
icon="fas fa-exclamation-circle"
className="Button Button--icon Button--flat Button--danger"
onclick={() => app.modal.show(WhyNotModal, { package: extension.name })}
aria-label={app.translator.trans('flarum-extension-manager.admin.extensions.check_why_it_failed_updating')}
/>
</Tooltip>
) : null}
</div>
</div>
);
}
version(v: string): string {
return v.charAt(0) === 'v' ? v.substring(1) : v;
}
}

View File

@@ -23,7 +23,7 @@ export default class Installer extends Component<InstallerAttrs> {
<label htmlFor="install-extension">{app.translator.trans('flarum-extension-manager.admin.extensions.install')}</label>
<div className="helpText">
{app.translator.trans('flarum-extension-manager.admin.extensions.install_help', {
extiverse: <a href="https://extiverse.com">extiverse.com</a>,
link: <a href="https://flarum.org/extensions">flarum.org</a>,
semantic_link: <a href="https://devhints.io/semver" />,
code: <code />,
})}

View File

@@ -7,7 +7,7 @@ import Alert from 'flarum/common/components/Alert';
import { UpdatedPackage, UpdateState } from '../states/ControlSectionState';
import WhyNotModal from './WhyNotModal';
import ExtensionItem from './ExtensionItem';
import ExtensionCard from './ExtensionCard';
import classList from 'flarum/common/utils/classList';
export interface MajorUpdaterAttrs extends ComponentAttrs {
@@ -27,7 +27,6 @@ export default class MajorUpdater<T extends MajorUpdaterAttrs = MajorUpdaterAttr
}
view(): Mithril.Children {
// @todo move Form-group--danger class to core for reuse
return (
<div
className={classList('Form-group Form-group--danger ExtensionManager-majorUpdate', {
@@ -63,7 +62,7 @@ export default class MajorUpdater<T extends MajorUpdaterAttrs = MajorUpdaterAttr
{this.updateState.incompatibleExtensions.length ? (
<div className="ExtensionManager-majorUpdate-incompatibleExtensions ExtensionManager-extensions-grid">
{this.updateState.incompatibleExtensions.map((extension: string) => (
<ExtensionItem
<ExtensionCard
extension={app.data.extensions[extension.replace('flarum-', '').replace('flarum-ext-', '').replace('/', '-')]}
updates={{}}
onClickUpdate={null}

View File

@@ -1,40 +0,0 @@
import app from 'flarum/admin/app';
import Component, { ComponentAttrs } from 'flarum/common/Component';
import Button from 'flarum/common/components/Button';
import QueueState from '../states/QueueState';
interface PaginationAttrs extends ComponentAttrs {
list: QueueState;
}
/**
* @todo make it abstract in core for reusability.
*/
export default class Pagination extends Component<PaginationAttrs> {
view() {
return (
<nav className="Pagination UserListPage-gridPagination">
<Button
disabled={!this.attrs.list.hasPrev() || app.extensionManager.control.isLoading()}
title={app.translator.trans('core.admin.users.pagination.back_button')}
onclick={() => this.attrs.list.prev()}
icon="fas fa-chevron-left"
className="Button Button--icon UserListPage-backBtn"
/>
<span className="UserListPage-pageNumber">
{app.translator.trans('core.admin.users.pagination.page_counter', {
current: this.attrs.list.pageNumber() + 1,
total: this.attrs.list.getTotalPages(),
})}
</span>
<Button
disabled={!this.attrs.list.hasNext() || app.extensionManager.control.isLoading()}
title={app.translator.trans('core.admin.users.pagination.next_button')}
onclick={() => this.attrs.list.next()}
icon="fas fa-chevron-right"
className="Button Button--icon UserListPage-nextBtn"
/>
</nav>
);
}
}

View File

@@ -9,12 +9,13 @@ import Icon from 'flarum/common/components/Icon';
import ItemList from 'flarum/common/utils/ItemList';
import extractText from 'flarum/common/utils/extractText';
import Link from 'flarum/common/components/Link';
import Pagination from 'flarum/common/components/Pagination';
import classList from 'flarum/common/utils/classList';
import Label from './Label';
import TaskOutputModal from './TaskOutputModal';
import humanDuration from '../utils/humanDuration';
import Task, { TaskOperations } from '../models/Task';
import Pagination from './Pagination';
interface QueueTableColumn extends ComponentAttrs {
label: string;
@@ -30,7 +31,7 @@ export default class QueueSection extends Component<{}> {
view() {
return (
<section id="ExtensionManager-queueSection" className="ExtensionPage-permissions ExtensionManager-queueSection">
<section id="ExtensionManager-queueSection" className="ExtensionPage-settings ExtensionManager-queueSection">
<div className="ExtensionPage-permissions-header ExtensionManager-queueSection-header">
<div className="container">
<h2 className="ExtensionTitle">{app.translator.trans('flarum-extension-manager.admin.sections.queue.title')}</h2>
@@ -174,32 +175,43 @@ export default class QueueSection extends Component<{}> {
return (
<>
<table className="Table ExtensionManager-queueTable">
<thead>
<tr>
{columns.toArray().map((item, index) => (
<th key={index}>{item.label}</th>
))}
</tr>
</thead>
<tbody>
{tasks.map((task, index) => (
<tr key={index}>
{columns.toArray().map((item, index) => {
const { label, content, ...attrs } = item;
return (
<td key={index} {...attrs}>
{content(task)}
</td>
);
})}
<div
className={classList('Table-container', {
'loading-container': tasks && app.extensionManager.queue.isLoading(),
})}
>
<table className="Table ExtensionManager-queueTable">
<thead>
<tr>
{columns.toArray().map((item, index) => (
<th key={index}>{item.label}</th>
))}
</tr>
))}
</tbody>
</table>
</thead>
<tbody>
{tasks.map((task, index) => (
<tr key={index}>
{columns.toArray().map((item, index) => {
const { label, content, ...attrs } = item;
<Pagination list={app.extensionManager.queue} />
return (
<td key={index} {...attrs}>
{content(task)}
</td>
);
})}
</tr>
))}
</tbody>
</table>
{tasks && app.extensionManager.queue.isLoading() && <LoadingIndicator size="large" />}
</div>
<Pagination
total={app.extensionManager.queue.getTotalItems()}
currentPage={app.extensionManager.queue.pageNumber() + 1}
perPage={app.extensionManager.queue.getPerPage()}
onChange={(page: number) => app.extensionManager.queue.goto(page)}
/>
</>
);
}

View File

@@ -6,36 +6,32 @@ import ItemList from 'flarum/common/utils/ItemList';
import QueueSection from './QueueSection';
import ControlSection from './ControlSection';
import ConfigureComposer from './ConfigureComposer';
import Alert from 'flarum/common/components/Alert';
import listItems from 'flarum/common/helpers/listItems';
import ConfigureAuth from './ConfigureAuth';
import DiscoverSection from './DiscoverSection';
export default class SettingsPage extends ExtensionPage {
content() {
const settings = app.registry.getSettings(this.extension.id);
const warnings = [app.translator.trans('flarum-extension-manager.admin.settings.access_warning')];
if (app.data.debugEnabled) warnings.push(app.translator.trans('flarum-extension-manager.admin.settings.debug_mode_warning'));
return (
<div className="ExtensionPage-settings">
<div className="container">
<div className="ExtensionManager-warnings Form-group">
<Alert className="ExtensionManager-primaryWarning" type="warning" dismissible={false}>
<ul>{listItems(warnings)}</ul>
</Alert>
</div>
{settings ? (
<div className="FormSectionGroup ExtensionManager-SettingsGroups">
<div className="FormSection">
<label>{app.translator.trans('flarum-extension-manager.admin.settings.title')}</label>
<div className="Form">{settings.map(this.buildSettingComponent.bind(this))}</div>
<div className="Form-group Form--controls">{this.submitButton()}</div>
</div>
<ConfigureComposer buildSettingComponent={this.buildSettingComponent} />
<ConfigureAuth buildSettingComponent={this.buildSettingComponent} />
</div>
[
<div className="Form-group">
<label>{app.translator.trans('flarum-extension-manager.admin.sections.settings.title')}</label>
<div className="helpText">{app.translator.trans('flarum-extension-manager.admin.sections.settings.description')}</div>
</div>,
<div className="FormSectionGroup ExtensionManager-SettingsGroups">
<div className="FormSection">
<label>{app.translator.trans('flarum-extension-manager.admin.settings.title')}</label>
<div className="Form">{settings.map(this.buildSettingComponent.bind(this))}</div>
<div className="Form-group Form--controls">{this.submitButton()}</div>
</div>
<ConfigureComposer buildSettingComponent={this.buildSettingComponent} />
<ConfigureAuth buildSettingComponent={this.buildSettingComponent} />
</div>,
]
) : (
<h3 className="ExtensionPage-subHeader">{app.translator.trans('core.admin.extension.no_settings')}</h3>
)}
@@ -47,9 +43,11 @@ export default class SettingsPage extends ExtensionPage {
sections(vnode: Mithril.VnodeDOM<ExtensionPageAttrs, this>): ItemList<unknown> {
const items = super.sections(vnode);
items.setPriority('content', 10);
items.add('discover', <DiscoverSection />, 15);
items.add('control', <ControlSection />, 8);
items.add('control', <ControlSection />, 10);
items.setPriority('content', 8);
if (app.data.settings['flarum-extension-manager.queue_jobs'] !== '0' && app.data.settings['flarum-extension-manager.queue_jobs']) {
items.add('queue', <QueueSection />, 5);

View File

@@ -4,9 +4,9 @@ import Button from 'flarum/common/components/Button';
import humanTime from 'flarum/common/helpers/humanTime';
import LoadingIndicator from 'flarum/common/components/LoadingIndicator';
import MajorUpdater from './MajorUpdater';
import ExtensionItem from './ExtensionItem';
import { Extension } from 'flarum/admin/AdminApplication';
import ItemList from 'flarum/common/utils/ItemList';
import InfoTile from 'flarum/common/components/InfoTile';
import ExtensionCard from './ExtensionCard';
export interface IUpdaterAttrs extends ComponentAttrs {}
@@ -59,8 +59,8 @@ export default class Updater extends Component<IUpdaterAttrs> {
if (!(state.extensionUpdates.length || hasMinorCoreUpdate)) {
return (
<div className="ExtensionManager-extensions">
<span className="helpText">{app.translator.trans('flarum-extension-manager.admin.updater.up_to_date')}</span>
<div className="ExtensionManager-extensions ExtensionManager-extensions--empty">
<InfoTile icon="fas fa-plug-circle-check">{app.translator.trans('flarum-extension-manager.admin.updater.up_to_date')}</InfoTile>
</div>
);
}
@@ -69,7 +69,7 @@ export default class Updater extends Component<IUpdaterAttrs> {
<div className="ExtensionManager-extensions">
<div className="ExtensionManager-extensions-grid">
{hasMinorCoreUpdate ? (
<ExtensionItem
<ExtensionCard
extension={state.coreUpdate!.extension}
updates={state.coreUpdate!.package}
isCore={true}
@@ -77,8 +77,8 @@ export default class Updater extends Component<IUpdaterAttrs> {
whyNotWarning={state.lastUpdateRun.limitedPackages().includes('flarum/core')}
/>
) : null}
{state.extensionUpdates.map((extension: Extension) => (
<ExtensionItem
{state.extensionUpdates.map((extension) => (
<ExtensionCard
extension={extension}
updates={state.packageUpdates[extension.id]}
onClickUpdate={{

View File

@@ -2,8 +2,14 @@ import Extend from 'flarum/common/extenders';
import app from 'flarum/admin/app';
import extractText from 'flarum/common/utils/extractText';
import SettingsPage from './components/SettingsPage';
import Task from './models/Task';
import ExternalExtension from './models/ExternalExtension';
export default [
new Extend.Store() //
.add('extension-manager-tasks', Task)
.add('external-extensions', ExternalExtension),
new Extend.Admin()
.setting(() => ({
setting: 'flarum-extension-manager.queue_jobs',

View File

@@ -4,7 +4,6 @@ import ExtensionPage from 'flarum/admin/components/ExtensionPage';
import Button from 'flarum/common/components/Button';
import LoadingModal from 'flarum/admin/components/LoadingModal';
import isExtensionEnabled from 'flarum/admin/utils/isExtensionEnabled';
import Task from './models/Task';
import jumpToQueue from './utils/jumpToQueue';
import { AsyncBackendResponse } from './shims';
import ExtensionManagerState from './states/ExtensionManagerState';
@@ -12,8 +11,6 @@ import ExtensionManagerState from './states/ExtensionManagerState';
export { default as extend } from './extend';
app.initializers.add('flarum-extension-manager', (app) => {
app.store.models['extension-manager-tasks'] = Task;
app.extensionManager = new ExtensionManagerState();
if (app.data['flarum-extension-manager.using_sync_queue']) {

View File

@@ -0,0 +1,73 @@
import Model from 'flarum/common/Model';
import app from 'flarum/admin/app';
import type { Extension } from 'flarum/admin/AdminApplication';
export default class ExternalExtension extends Model {
extensionId = Model.attribute<string>('extensionId');
name = Model.attribute<string>('name');
title = Model.attribute<string>('title');
description = Model.attribute<string>('description');
iconUrl = Model.attribute<string>('iconUrl');
icon = Model.attribute<{
name: string;
[key: string]: string;
}>('icon');
highestVersion = Model.attribute<string>('highestVersion');
httpUri = Model.attribute<string>('httpUri');
discussUri = Model.attribute<string>('discussUri');
vendor = Model.attribute<string>('vendor');
isPremium = Model.attribute<boolean>('isPremium');
isLocale = Model.attribute<boolean>('isLocale');
locale = Model.attribute<string>('locale');
latestFlarumVersionSupported = Model.attribute<string>('latestFlarumVersionSupported');
downloads = Model.attribute<number>('downloads');
readonly installed = false;
public isSupported(): boolean {
const currentVersion = app.data.settings.version;
const latestCompatibleVersion = this.latestFlarumVersionSupported();
// If stability is not the same, it's not compatible.
if (currentVersion.split('-')[1] !== latestCompatibleVersion.split('-')[1]) {
return false;
}
// Minor versions are compatible.
return currentVersion.split('.')[0] === latestCompatibleVersion.split('.')[0];
}
public isStable(): boolean {
const split = this.highestVersion().split('-');
if (split.length === 1) {
return true;
}
const stability = split[1].split('.');
return stability[0] === 'stable';
}
public toLocalExtension(): Extension {
return {
id: this.extensionId(),
name: this.name(),
version: this.highestVersion(),
description: this.description(),
icon: this.icon() || {
name: 'fas fa-box-open',
backgroundColor: '#117187',
color: '#fff',
},
links: {
discuss: this.discussUri(),
website: this.httpUri(),
},
extra: {
'flarum-extension': {
title: this.title(),
},
},
};
}
}

View File

@@ -0,0 +1,32 @@
import app from 'flarum/admin/app';
import PaginatedListState, { SortMap } from 'flarum/common/states/PaginatedListState';
import ExternalExtension from '../models/ExternalExtension';
export default class ExtensionListState extends PaginatedListState<ExternalExtension> {
get type(): string {
return 'external-extensions';
}
constructor() {
super(
{
sort: '-downloads',
},
1,
12
);
}
sortMap(): SortMap {
return {
'-createdAt': {
sort: '-createdAt',
label: app.translator.trans('flarum-extension-manager.admin.sections.discover.sort.latest', {}, true),
},
'-downloads': {
sort: '-downloads',
label: app.translator.trans('flarum-extension-manager.admin.sections.discover.sort.top', {}, true),
},
};
}
}

View File

@@ -1,7 +1,9 @@
import QueueState from './QueueState';
import ControlSectionState from './ControlSectionState';
import ExtensionListState from './ExtensionListState';
export default class ExtensionManagerState {
public queue: QueueState = new QueueState();
public control: ControlSectionState = new ControlSectionState();
public extensions: ExtensionListState = new ExtensionListState();
}

View File

@@ -8,9 +8,10 @@ export default class QueueState {
private limit = 20;
private offset = 0;
private total = 0;
private loading = false;
load(params?: ApiQueryParamsPlural, actionTaken = false): Promise<Task[]> {
this.tasks = null;
this.loading = true;
params = {
page: {
limit: this.limit,
@@ -22,7 +23,7 @@ export default class QueueState {
return app.store.find<Task[]>('extension-manager-tasks', params || {}).then((data) => {
this.tasks = data;
this.total = data.payload.meta?.total || 0;
this.total = data.payload.meta?.page?.total || 0;
m.redraw();
@@ -40,14 +41,24 @@ export default class QueueState {
app.extensionManager.control.setLoading(null);
}
this.loading = false;
return data;
});
}
isLoading() {
return this.loading;
}
getItems() {
return this.tasks;
}
getTotalItems() {
return this.total;
}
getTotalPages(): number {
return Math.ceil(this.total / this.limit);
}
@@ -56,6 +67,10 @@ export default class QueueState {
return Math.ceil(this.offset / this.limit);
}
getPerPage() {
return this.limit;
}
hasPrev(): boolean {
return this.pageNumber() !== 0;
}
@@ -78,6 +93,11 @@ export default class QueueState {
}
}
goto(page: number): void {
this.offset = (page - 1) * this.limit;
this.load();
}
pollQueue(actionTaken = false): void {
if (this.polling) {
clearTimeout(this.polling);