mirror of
https://github.com/flarum/framework.git
synced 2024-11-22 14:04:03 +08:00
feat: extension list UI (#4066)
This commit is contained in:
parent
b0e8f5ca36
commit
0107c96fb7
|
@ -10,7 +10,9 @@
|
||||||
namespace Flarum\ExtensionManager;
|
namespace Flarum\ExtensionManager;
|
||||||
|
|
||||||
use Flarum\Extend;
|
use Flarum\Extend;
|
||||||
|
use Flarum\ExtensionManager\Api\Resource\ExternalExtensionResource;
|
||||||
use Flarum\ExtensionManager\Api\Resource\TaskResource;
|
use Flarum\ExtensionManager\Api\Resource\TaskResource;
|
||||||
|
use Flarum\ExtensionManager\Exception\CannotFetchExternalExtension;
|
||||||
use Flarum\Foundation\Paths;
|
use Flarum\Foundation\Paths;
|
||||||
use Flarum\Frontend\Document;
|
use Flarum\Frontend\Document;
|
||||||
use Illuminate\Contracts\Queue\Queue;
|
use Illuminate\Contracts\Queue\Queue;
|
||||||
|
@ -29,6 +31,7 @@ return [
|
||||||
->post('/extension-manager/composer', 'extension-manager.composer', Api\Controller\ConfigureComposerController::class),
|
->post('/extension-manager/composer', 'extension-manager.composer', Api\Controller\ConfigureComposerController::class),
|
||||||
|
|
||||||
new Extend\ApiResource(TaskResource::class),
|
new Extend\ApiResource(TaskResource::class),
|
||||||
|
new Extend\ApiResource(ExternalExtensionResource::class),
|
||||||
|
|
||||||
(new Extend\Frontend('admin'))
|
(new Extend\Frontend('admin'))
|
||||||
->css(__DIR__.'/less/admin.less')
|
->css(__DIR__.'/less/admin.less')
|
||||||
|
@ -62,8 +65,10 @@ return [
|
||||||
->handler(Exception\ComposerRequireFailedException::class, Exception\ExceptionHandler::class)
|
->handler(Exception\ComposerRequireFailedException::class, Exception\ExceptionHandler::class)
|
||||||
->handler(Exception\ComposerUpdateFailedException::class, Exception\ExceptionHandler::class)
|
->handler(Exception\ComposerUpdateFailedException::class, Exception\ExceptionHandler::class)
|
||||||
->handler(Exception\MajorUpdateFailedException::class, Exception\ExceptionHandler::class)
|
->handler(Exception\MajorUpdateFailedException::class, Exception\ExceptionHandler::class)
|
||||||
|
->type(CannotFetchExternalExtension::class, 'cannot_fetch_external_extension')
|
||||||
->status('extension_already_installed', 409)
|
->status('extension_already_installed', 409)
|
||||||
->status('extension_not_installed', 409)
|
->status('extension_not_installed', 409)
|
||||||
->status('no_new_major_version', 409)
|
->status('no_new_major_version', 409)
|
||||||
->status('extension_not_directly_dependency', 409),
|
->status('extension_not_directly_dependency', 409)
|
||||||
|
->status('cannot_fetch_external_extension', 503),
|
||||||
];
|
];
|
||||||
|
|
|
@ -15,12 +15,7 @@ export default class ControlSection extends Component<ComponentAttrs> {
|
||||||
|
|
||||||
view() {
|
view() {
|
||||||
return (
|
return (
|
||||||
<div className="ExtensionPage-permissions ExtensionManager-controlSection">
|
<div className="ExtensionPage-settings 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="container">
|
<div className="container">
|
||||||
{app.data['flarum-extension-manager.writable_dirs'] ? (
|
{app.data['flarum-extension-manager.writable_dirs'] ? (
|
||||||
<Form>
|
<Form>
|
||||||
|
|
|
@ -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');
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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>
|
<label htmlFor="install-extension">{app.translator.trans('flarum-extension-manager.admin.extensions.install')}</label>
|
||||||
<div className="helpText">
|
<div className="helpText">
|
||||||
{app.translator.trans('flarum-extension-manager.admin.extensions.install_help', {
|
{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" />,
|
semantic_link: <a href="https://devhints.io/semver" />,
|
||||||
code: <code />,
|
code: <code />,
|
||||||
})}
|
})}
|
||||||
|
|
|
@ -7,7 +7,7 @@ import Alert from 'flarum/common/components/Alert';
|
||||||
|
|
||||||
import { UpdatedPackage, UpdateState } from '../states/ControlSectionState';
|
import { UpdatedPackage, UpdateState } from '../states/ControlSectionState';
|
||||||
import WhyNotModal from './WhyNotModal';
|
import WhyNotModal from './WhyNotModal';
|
||||||
import ExtensionItem from './ExtensionItem';
|
import ExtensionCard from './ExtensionCard';
|
||||||
import classList from 'flarum/common/utils/classList';
|
import classList from 'flarum/common/utils/classList';
|
||||||
|
|
||||||
export interface MajorUpdaterAttrs extends ComponentAttrs {
|
export interface MajorUpdaterAttrs extends ComponentAttrs {
|
||||||
|
@ -27,7 +27,6 @@ export default class MajorUpdater<T extends MajorUpdaterAttrs = MajorUpdaterAttr
|
||||||
}
|
}
|
||||||
|
|
||||||
view(): Mithril.Children {
|
view(): Mithril.Children {
|
||||||
// @todo move Form-group--danger class to core for reuse
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={classList('Form-group Form-group--danger ExtensionManager-majorUpdate', {
|
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 ? (
|
{this.updateState.incompatibleExtensions.length ? (
|
||||||
<div className="ExtensionManager-majorUpdate-incompatibleExtensions ExtensionManager-extensions-grid">
|
<div className="ExtensionManager-majorUpdate-incompatibleExtensions ExtensionManager-extensions-grid">
|
||||||
{this.updateState.incompatibleExtensions.map((extension: string) => (
|
{this.updateState.incompatibleExtensions.map((extension: string) => (
|
||||||
<ExtensionItem
|
<ExtensionCard
|
||||||
extension={app.data.extensions[extension.replace('flarum-', '').replace('flarum-ext-', '').replace('/', '-')]}
|
extension={app.data.extensions[extension.replace('flarum-', '').replace('flarum-ext-', '').replace('/', '-')]}
|
||||||
updates={{}}
|
updates={{}}
|
||||||
onClickUpdate={null}
|
onClickUpdate={null}
|
||||||
|
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -9,12 +9,13 @@ import Icon from 'flarum/common/components/Icon';
|
||||||
import ItemList from 'flarum/common/utils/ItemList';
|
import ItemList from 'flarum/common/utils/ItemList';
|
||||||
import extractText from 'flarum/common/utils/extractText';
|
import extractText from 'flarum/common/utils/extractText';
|
||||||
import Link from 'flarum/common/components/Link';
|
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 Label from './Label';
|
||||||
import TaskOutputModal from './TaskOutputModal';
|
import TaskOutputModal from './TaskOutputModal';
|
||||||
import humanDuration from '../utils/humanDuration';
|
import humanDuration from '../utils/humanDuration';
|
||||||
import Task, { TaskOperations } from '../models/Task';
|
import Task, { TaskOperations } from '../models/Task';
|
||||||
import Pagination from './Pagination';
|
|
||||||
|
|
||||||
interface QueueTableColumn extends ComponentAttrs {
|
interface QueueTableColumn extends ComponentAttrs {
|
||||||
label: string;
|
label: string;
|
||||||
|
@ -30,7 +31,7 @@ export default class QueueSection extends Component<{}> {
|
||||||
|
|
||||||
view() {
|
view() {
|
||||||
return (
|
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="ExtensionPage-permissions-header ExtensionManager-queueSection-header">
|
||||||
<div className="container">
|
<div className="container">
|
||||||
<h2 className="ExtensionTitle">{app.translator.trans('flarum-extension-manager.admin.sections.queue.title')}</h2>
|
<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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<table className="Table ExtensionManager-queueTable">
|
<div
|
||||||
<thead>
|
className={classList('Table-container', {
|
||||||
<tr>
|
'loading-container': tasks && app.extensionManager.queue.isLoading(),
|
||||||
{columns.toArray().map((item, index) => (
|
})}
|
||||||
<th key={index}>{item.label}</th>
|
>
|
||||||
))}
|
<table className="Table ExtensionManager-queueTable">
|
||||||
</tr>
|
<thead>
|
||||||
</thead>
|
<tr>
|
||||||
<tbody>
|
{columns.toArray().map((item, index) => (
|
||||||
{tasks.map((task, index) => (
|
<th key={index}>{item.label}</th>
|
||||||
<tr key={index}>
|
))}
|
||||||
{columns.toArray().map((item, index) => {
|
|
||||||
const { label, content, ...attrs } = item;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<td key={index} {...attrs}>
|
|
||||||
{content(task)}
|
|
||||||
</td>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
</thead>
|
||||||
</tbody>
|
<tbody>
|
||||||
</table>
|
{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)}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,36 +6,32 @@ import ItemList from 'flarum/common/utils/ItemList';
|
||||||
import QueueSection from './QueueSection';
|
import QueueSection from './QueueSection';
|
||||||
import ControlSection from './ControlSection';
|
import ControlSection from './ControlSection';
|
||||||
import ConfigureComposer from './ConfigureComposer';
|
import ConfigureComposer from './ConfigureComposer';
|
||||||
import Alert from 'flarum/common/components/Alert';
|
|
||||||
import listItems from 'flarum/common/helpers/listItems';
|
|
||||||
import ConfigureAuth from './ConfigureAuth';
|
import ConfigureAuth from './ConfigureAuth';
|
||||||
|
import DiscoverSection from './DiscoverSection';
|
||||||
|
|
||||||
export default class SettingsPage extends ExtensionPage {
|
export default class SettingsPage extends ExtensionPage {
|
||||||
content() {
|
content() {
|
||||||
const settings = app.registry.getSettings(this.extension.id);
|
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 (
|
return (
|
||||||
<div className="ExtensionPage-settings">
|
<div className="ExtensionPage-settings">
|
||||||
<div className="container">
|
<div className="container">
|
||||||
<div className="ExtensionManager-warnings Form-group">
|
|
||||||
<Alert className="ExtensionManager-primaryWarning" type="warning" dismissible={false}>
|
|
||||||
<ul>{listItems(warnings)}</ul>
|
|
||||||
</Alert>
|
|
||||||
</div>
|
|
||||||
{settings ? (
|
{settings ? (
|
||||||
<div className="FormSectionGroup ExtensionManager-SettingsGroups">
|
[
|
||||||
<div className="FormSection">
|
<div className="Form-group">
|
||||||
<label>{app.translator.trans('flarum-extension-manager.admin.settings.title')}</label>
|
<label>{app.translator.trans('flarum-extension-manager.admin.sections.settings.title')}</label>
|
||||||
<div className="Form">{settings.map(this.buildSettingComponent.bind(this))}</div>
|
<div className="helpText">{app.translator.trans('flarum-extension-manager.admin.sections.settings.description')}</div>
|
||||||
<div className="Form-group Form--controls">{this.submitButton()}</div>
|
</div>,
|
||||||
</div>
|
<div className="FormSectionGroup ExtensionManager-SettingsGroups">
|
||||||
<ConfigureComposer buildSettingComponent={this.buildSettingComponent} />
|
<div className="FormSection">
|
||||||
<ConfigureAuth buildSettingComponent={this.buildSettingComponent} />
|
<label>{app.translator.trans('flarum-extension-manager.admin.settings.title')}</label>
|
||||||
</div>
|
<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>
|
<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> {
|
sections(vnode: Mithril.VnodeDOM<ExtensionPageAttrs, this>): ItemList<unknown> {
|
||||||
const items = super.sections(vnode);
|
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']) {
|
if (app.data.settings['flarum-extension-manager.queue_jobs'] !== '0' && app.data.settings['flarum-extension-manager.queue_jobs']) {
|
||||||
items.add('queue', <QueueSection />, 5);
|
items.add('queue', <QueueSection />, 5);
|
||||||
|
|
|
@ -4,9 +4,9 @@ import Button from 'flarum/common/components/Button';
|
||||||
import humanTime from 'flarum/common/helpers/humanTime';
|
import humanTime from 'flarum/common/helpers/humanTime';
|
||||||
import LoadingIndicator from 'flarum/common/components/LoadingIndicator';
|
import LoadingIndicator from 'flarum/common/components/LoadingIndicator';
|
||||||
import MajorUpdater from './MajorUpdater';
|
import MajorUpdater from './MajorUpdater';
|
||||||
import ExtensionItem from './ExtensionItem';
|
|
||||||
import { Extension } from 'flarum/admin/AdminApplication';
|
|
||||||
import ItemList from 'flarum/common/utils/ItemList';
|
import ItemList from 'flarum/common/utils/ItemList';
|
||||||
|
import InfoTile from 'flarum/common/components/InfoTile';
|
||||||
|
import ExtensionCard from './ExtensionCard';
|
||||||
|
|
||||||
export interface IUpdaterAttrs extends ComponentAttrs {}
|
export interface IUpdaterAttrs extends ComponentAttrs {}
|
||||||
|
|
||||||
|
@ -59,8 +59,8 @@ export default class Updater extends Component<IUpdaterAttrs> {
|
||||||
|
|
||||||
if (!(state.extensionUpdates.length || hasMinorCoreUpdate)) {
|
if (!(state.extensionUpdates.length || hasMinorCoreUpdate)) {
|
||||||
return (
|
return (
|
||||||
<div className="ExtensionManager-extensions">
|
<div className="ExtensionManager-extensions ExtensionManager-extensions--empty">
|
||||||
<span className="helpText">{app.translator.trans('flarum-extension-manager.admin.updater.up_to_date')}</span>
|
<InfoTile icon="fas fa-plug-circle-check">{app.translator.trans('flarum-extension-manager.admin.updater.up_to_date')}</InfoTile>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -69,7 +69,7 @@ export default class Updater extends Component<IUpdaterAttrs> {
|
||||||
<div className="ExtensionManager-extensions">
|
<div className="ExtensionManager-extensions">
|
||||||
<div className="ExtensionManager-extensions-grid">
|
<div className="ExtensionManager-extensions-grid">
|
||||||
{hasMinorCoreUpdate ? (
|
{hasMinorCoreUpdate ? (
|
||||||
<ExtensionItem
|
<ExtensionCard
|
||||||
extension={state.coreUpdate!.extension}
|
extension={state.coreUpdate!.extension}
|
||||||
updates={state.coreUpdate!.package}
|
updates={state.coreUpdate!.package}
|
||||||
isCore={true}
|
isCore={true}
|
||||||
|
@ -77,8 +77,8 @@ export default class Updater extends Component<IUpdaterAttrs> {
|
||||||
whyNotWarning={state.lastUpdateRun.limitedPackages().includes('flarum/core')}
|
whyNotWarning={state.lastUpdateRun.limitedPackages().includes('flarum/core')}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
{state.extensionUpdates.map((extension: Extension) => (
|
{state.extensionUpdates.map((extension) => (
|
||||||
<ExtensionItem
|
<ExtensionCard
|
||||||
extension={extension}
|
extension={extension}
|
||||||
updates={state.packageUpdates[extension.id]}
|
updates={state.packageUpdates[extension.id]}
|
||||||
onClickUpdate={{
|
onClickUpdate={{
|
||||||
|
|
|
@ -2,8 +2,14 @@ import Extend from 'flarum/common/extenders';
|
||||||
import app from 'flarum/admin/app';
|
import app from 'flarum/admin/app';
|
||||||
import extractText from 'flarum/common/utils/extractText';
|
import extractText from 'flarum/common/utils/extractText';
|
||||||
import SettingsPage from './components/SettingsPage';
|
import SettingsPage from './components/SettingsPage';
|
||||||
|
import Task from './models/Task';
|
||||||
|
import ExternalExtension from './models/ExternalExtension';
|
||||||
|
|
||||||
export default [
|
export default [
|
||||||
|
new Extend.Store() //
|
||||||
|
.add('extension-manager-tasks', Task)
|
||||||
|
.add('external-extensions', ExternalExtension),
|
||||||
|
|
||||||
new Extend.Admin()
|
new Extend.Admin()
|
||||||
.setting(() => ({
|
.setting(() => ({
|
||||||
setting: 'flarum-extension-manager.queue_jobs',
|
setting: 'flarum-extension-manager.queue_jobs',
|
||||||
|
|
|
@ -4,7 +4,6 @@ import ExtensionPage from 'flarum/admin/components/ExtensionPage';
|
||||||
import Button from 'flarum/common/components/Button';
|
import Button from 'flarum/common/components/Button';
|
||||||
import LoadingModal from 'flarum/admin/components/LoadingModal';
|
import LoadingModal from 'flarum/admin/components/LoadingModal';
|
||||||
import isExtensionEnabled from 'flarum/admin/utils/isExtensionEnabled';
|
import isExtensionEnabled from 'flarum/admin/utils/isExtensionEnabled';
|
||||||
import Task from './models/Task';
|
|
||||||
import jumpToQueue from './utils/jumpToQueue';
|
import jumpToQueue from './utils/jumpToQueue';
|
||||||
import { AsyncBackendResponse } from './shims';
|
import { AsyncBackendResponse } from './shims';
|
||||||
import ExtensionManagerState from './states/ExtensionManagerState';
|
import ExtensionManagerState from './states/ExtensionManagerState';
|
||||||
|
@ -12,8 +11,6 @@ import ExtensionManagerState from './states/ExtensionManagerState';
|
||||||
export { default as extend } from './extend';
|
export { default as extend } from './extend';
|
||||||
|
|
||||||
app.initializers.add('flarum-extension-manager', (app) => {
|
app.initializers.add('flarum-extension-manager', (app) => {
|
||||||
app.store.models['extension-manager-tasks'] = Task;
|
|
||||||
|
|
||||||
app.extensionManager = new ExtensionManagerState();
|
app.extensionManager = new ExtensionManagerState();
|
||||||
|
|
||||||
if (app.data['flarum-extension-manager.using_sync_queue']) {
|
if (app.data['flarum-extension-manager.using_sync_queue']) {
|
||||||
|
|
|
@ -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(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -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),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,7 +1,9 @@
|
||||||
import QueueState from './QueueState';
|
import QueueState from './QueueState';
|
||||||
import ControlSectionState from './ControlSectionState';
|
import ControlSectionState from './ControlSectionState';
|
||||||
|
import ExtensionListState from './ExtensionListState';
|
||||||
|
|
||||||
export default class ExtensionManagerState {
|
export default class ExtensionManagerState {
|
||||||
public queue: QueueState = new QueueState();
|
public queue: QueueState = new QueueState();
|
||||||
public control: ControlSectionState = new ControlSectionState();
|
public control: ControlSectionState = new ControlSectionState();
|
||||||
|
public extensions: ExtensionListState = new ExtensionListState();
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,9 +8,10 @@ export default class QueueState {
|
||||||
private limit = 20;
|
private limit = 20;
|
||||||
private offset = 0;
|
private offset = 0;
|
||||||
private total = 0;
|
private total = 0;
|
||||||
|
private loading = false;
|
||||||
|
|
||||||
load(params?: ApiQueryParamsPlural, actionTaken = false): Promise<Task[]> {
|
load(params?: ApiQueryParamsPlural, actionTaken = false): Promise<Task[]> {
|
||||||
this.tasks = null;
|
this.loading = true;
|
||||||
params = {
|
params = {
|
||||||
page: {
|
page: {
|
||||||
limit: this.limit,
|
limit: this.limit,
|
||||||
|
@ -22,7 +23,7 @@ export default class QueueState {
|
||||||
|
|
||||||
return app.store.find<Task[]>('extension-manager-tasks', params || {}).then((data) => {
|
return app.store.find<Task[]>('extension-manager-tasks', params || {}).then((data) => {
|
||||||
this.tasks = data;
|
this.tasks = data;
|
||||||
this.total = data.payload.meta?.total || 0;
|
this.total = data.payload.meta?.page?.total || 0;
|
||||||
|
|
||||||
m.redraw();
|
m.redraw();
|
||||||
|
|
||||||
|
@ -40,14 +41,24 @@ export default class QueueState {
|
||||||
app.extensionManager.control.setLoading(null);
|
app.extensionManager.control.setLoading(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.loading = false;
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isLoading() {
|
||||||
|
return this.loading;
|
||||||
|
}
|
||||||
|
|
||||||
getItems() {
|
getItems() {
|
||||||
return this.tasks;
|
return this.tasks;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getTotalItems() {
|
||||||
|
return this.total;
|
||||||
|
}
|
||||||
|
|
||||||
getTotalPages(): number {
|
getTotalPages(): number {
|
||||||
return Math.ceil(this.total / this.limit);
|
return Math.ceil(this.total / this.limit);
|
||||||
}
|
}
|
||||||
|
@ -56,6 +67,10 @@ export default class QueueState {
|
||||||
return Math.ceil(this.offset / this.limit);
|
return Math.ceil(this.offset / this.limit);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getPerPage() {
|
||||||
|
return this.limit;
|
||||||
|
}
|
||||||
|
|
||||||
hasPrev(): boolean {
|
hasPrev(): boolean {
|
||||||
return this.pageNumber() !== 0;
|
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 {
|
pollQueue(actionTaken = false): void {
|
||||||
if (this.polling) {
|
if (this.polling) {
|
||||||
clearTimeout(this.polling);
|
clearTimeout(this.polling);
|
||||||
|
|
|
@ -2,6 +2,8 @@
|
||||||
@import "admin/TaskOutputModal";
|
@import "admin/TaskOutputModal";
|
||||||
@import "admin/QueueSection";
|
@import "admin/QueueSection";
|
||||||
@import "admin/ControlSection";
|
@import "admin/ControlSection";
|
||||||
|
@import "admin/DiscoverSection";
|
||||||
|
@import "admin/ExtensionCard";
|
||||||
|
|
||||||
.ExtensionManager-controlSection {
|
.ExtensionManager-controlSection {
|
||||||
> .container {
|
> .container {
|
||||||
|
@ -16,12 +18,6 @@
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.Form-group--danger {
|
|
||||||
border: 2px solid var(--alert-error-bg);
|
|
||||||
border-radius: var(--border-radius);
|
|
||||||
background-color: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ButtonGroup--full {
|
.ButtonGroup--full {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -47,6 +43,7 @@
|
||||||
column-count: 3;
|
column-count: 3;
|
||||||
column-gap: 30px;
|
column-gap: 30px;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
margin-top: 24px;
|
||||||
|
|
||||||
.FormSection {
|
.FormSection {
|
||||||
min-width: 300px;
|
min-width: 300px;
|
||||||
|
@ -63,11 +60,3 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.ExtensionManager-warnings {
|
|
||||||
margin-bottom: 24px;
|
|
||||||
|
|
||||||
> .Alert {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -25,62 +25,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.ExtensionManager-extension {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
background-color: var(--control-bg);
|
|
||||||
padding: 8px;
|
|
||||||
border-radius: var(--border-radius);
|
|
||||||
|
|
||||||
&-controls {
|
|
||||||
margin-left: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-icon {
|
|
||||||
--size: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-name {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-version {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
|
|
||||||
&-latest {
|
|
||||||
text-transform: lowercase;
|
|
||||||
padding: 2px 6px;
|
|
||||||
font-size: 0.7rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&--core {
|
|
||||||
--bg-hover: darken(#e7672e, 5);
|
|
||||||
background-color: #e7672e;
|
|
||||||
color: #fff;
|
|
||||||
--button-color: #fff;
|
|
||||||
--button-bg-hover: var(--bg-hover);
|
|
||||||
|
|
||||||
.Button--danger {
|
|
||||||
color: #fff;
|
|
||||||
--button-bg-hover: var(--bg-hover);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&--core &-icon {
|
|
||||||
background-size: 100%;
|
|
||||||
background-color: transparent;
|
|
||||||
filter: grayscale(1) brightness(3.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
&--danger {
|
|
||||||
background-color: var(--control-danger-bg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.ExtensionManager-majorUpdate {
|
.ExtensionManager-majorUpdate {
|
||||||
--space: 16px;
|
--space: 16px;
|
||||||
padding: var(--space);
|
padding: var(--space);
|
||||||
|
@ -169,3 +113,8 @@
|
||||||
.ExtensionManager-primaryWarning ul {
|
.ExtensionManager-primaryWarning ul {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ExtensionManager-extensions--empty {
|
||||||
|
border: 1px solid var(--control-bg);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
}
|
||||||
|
|
51
extensions/package-manager/less/admin/DiscoverSection.less
Normal file
51
extensions/package-manager/less/admin/DiscoverSection.less
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
.ExtensionManager-DiscoverSection .Tabs-divider {
|
||||||
|
margin-left: -30px;
|
||||||
|
margin-right: -30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ExtensionManager-DiscoverSection-list-inner {
|
||||||
|
--cards: 1;
|
||||||
|
--gap: 24px;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, calc((100% / var(--cards)) - (var(--gap) - (var(--gap) / var(--cards)))));
|
||||||
|
gap: var(--gap);
|
||||||
|
|
||||||
|
&--empty {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media @tablet-up {
|
||||||
|
--cards: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media @desktop-xl {
|
||||||
|
--cards: 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media @desktop-xxl {
|
||||||
|
--cards: 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media @desktop-xxxl {
|
||||||
|
--cards: 5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ExtensionManager-DiscoverSection-toolbar {
|
||||||
|
margin-bottom: 14px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
&-primary, &-secondary {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ExtensionManager-DiscoverSection-footer {
|
||||||
|
margin: 24px 0 0;
|
||||||
|
|
||||||
|
> * {
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
}
|
128
extensions/package-manager/less/admin/ExtensionCard.less
Normal file
128
extensions/package-manager/less/admin/ExtensionCard.less
Normal file
|
@ -0,0 +1,128 @@
|
||||||
|
.ExtensionCard {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
padding: 12px;
|
||||||
|
border: 1px solid var(--control-bg);
|
||||||
|
color: var(--control-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ExtensionCard-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ExtensionCard-header .ExtensionIcon {
|
||||||
|
--size: 36px;
|
||||||
|
background-color: transparent;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ExtensionCard-badges {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ExtensionCard-badge--premium {
|
||||||
|
--badge-bg: #FBDB33;
|
||||||
|
--badge-color: #4B4940;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ExtensionCard--core .ExtensionIcon, .ExtensionCard-badge--flarum::before {
|
||||||
|
filter: grayscale(1) brightness(3.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ExtensionCard-badge--flarum {
|
||||||
|
background-color: #e7672e;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
background-image: url('../extensions/flarum-extension-manager/flarum.svg');
|
||||||
|
background-size: 100%;
|
||||||
|
content: '';
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
width: 80%;
|
||||||
|
height: 80%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ExtensionCard-actions {
|
||||||
|
margin-inline-start: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ExtensionCard-header h4 {
|
||||||
|
color: var(--text-color);
|
||||||
|
font-size: 14px;
|
||||||
|
margin: 0;
|
||||||
|
max-width: 45%;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ExtensionCard-meta {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ExtensionCard-badges .Badge {
|
||||||
|
--size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ExtensionCard-meta > * {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ExtensionCard-body {
|
||||||
|
margin-bottom: 6px;
|
||||||
|
flex-grow: 1;
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ExtensionCard {
|
||||||
|
&-version {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
&-latest {
|
||||||
|
text-transform: lowercase;
|
||||||
|
padding: 2px 6px;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&--core {
|
||||||
|
--bg-hover: #e2571a;
|
||||||
|
--text-color: #fff;
|
||||||
|
--button-color: #fff;
|
||||||
|
--button-bg-hover: var(--bg-hover);
|
||||||
|
background-color: #e7672e;
|
||||||
|
border-color: var(--bg-hover);
|
||||||
|
color: #fff;
|
||||||
|
|
||||||
|
.Button--danger {
|
||||||
|
color: #fff;
|
||||||
|
--button-bg-hover: var(--bg-hover);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&--core .ExtensionIcon {
|
||||||
|
background-size: 100%;
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--danger {
|
||||||
|
background-color: var(--control-danger-bg);
|
||||||
|
border-color: var(--control-danger-bg-hover);
|
||||||
|
color: var(--control-danger-color);
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,7 +4,7 @@ flarum-extension-manager:
|
||||||
add_label: New authentication method
|
add_label: New authentication method
|
||||||
add_modal:
|
add_modal:
|
||||||
host_label: Host
|
host_label: Host
|
||||||
host_placeholder: "example: extiverse.com"
|
host_placeholder: "example: bearer.flarum.org"
|
||||||
submit_button: Submit
|
submit_button: Submit
|
||||||
token_label: Token
|
token_label: Token
|
||||||
type_label: Type
|
type_label: Type
|
||||||
|
@ -70,7 +70,7 @@ flarum-extension-manager:
|
||||||
install: Install a new extension
|
install: Install a new extension
|
||||||
install_help: >
|
install_help: >
|
||||||
Fill in the extension package name to proceed. You can specify a <semantic_link>semantic version</semantic_link> using the format <code>vendor/package-name:version</code>.
|
Fill in the extension package name to proceed. You can specify a <semantic_link>semantic version</semantic_link> using the format <code>vendor/package-name:version</code>.
|
||||||
Visit {extiverse} to browse extensions.
|
Visit {link} to browse extensions.
|
||||||
proceed: Proceed
|
proceed: Proceed
|
||||||
remove: Uninstall
|
remove: Uninstall
|
||||||
successful_install: "{extension} was installed successfully, redirecting.."
|
successful_install: "{extension} was installed successfully, redirecting.."
|
||||||
|
@ -100,8 +100,34 @@ flarum-extension-manager:
|
||||||
content: This will also update any other extensions/packages with availabe updates.
|
content: This will also update any other extensions/packages with availabe updates.
|
||||||
|
|
||||||
sections:
|
sections:
|
||||||
control:
|
discover:
|
||||||
title: Manager
|
description: Add new features and integrations to your Flarum forum with extensions.
|
||||||
|
empty_results: Looks like there are no extensions available.
|
||||||
|
extension:
|
||||||
|
badges:
|
||||||
|
compatible: Compatible
|
||||||
|
fof: Friends Of Flarum
|
||||||
|
flarum: Flarum
|
||||||
|
incompatible: Incompatible
|
||||||
|
premium: Premium
|
||||||
|
unstable: Unstable
|
||||||
|
downloads: "{count, plural, one {{formattedCount} download} other {{formattedCount} downloads}}"
|
||||||
|
premium_extension_terms: Premium extension terms
|
||||||
|
search: Search...
|
||||||
|
server_error: Service currently unavailable, please try again later.
|
||||||
|
sort:
|
||||||
|
toggle_dropdown_accessible_label: Toggle sort options
|
||||||
|
top: Most Downloads
|
||||||
|
latest: Latest
|
||||||
|
tabs:
|
||||||
|
discover: Discover
|
||||||
|
extensions: Extensions
|
||||||
|
languages: Languages
|
||||||
|
themes: Themes
|
||||||
|
title: Extensions
|
||||||
|
party_filter:
|
||||||
|
all: All
|
||||||
|
premium: Premium
|
||||||
queue:
|
queue:
|
||||||
columns:
|
columns:
|
||||||
details: Details
|
details: Details
|
||||||
|
@ -133,6 +159,9 @@ flarum-extension-manager:
|
||||||
running: Running
|
running: Running
|
||||||
task_just_started: Task just started
|
task_just_started: Task just started
|
||||||
title: Queue
|
title: Queue
|
||||||
|
settings:
|
||||||
|
title: Options and Queue
|
||||||
|
description: Configure the extension manager and check operations in the queue.
|
||||||
|
|
||||||
settings:
|
settings:
|
||||||
title: => core.ref.settings
|
title: => core.ref.settings
|
||||||
|
@ -148,7 +177,7 @@ flarum-extension-manager:
|
||||||
Set to 0 to keep all tasks.
|
Set to 0 to keep all tasks.
|
||||||
|
|
||||||
updater:
|
updater:
|
||||||
up_to_date: Everything is up to date!
|
up_to_date: No pending updates.
|
||||||
check_for_updates: Check for updates
|
check_for_updates: Check for updates
|
||||||
flarum: Flarum Core
|
flarum: Flarum Core
|
||||||
global_update_successful: Successfully updated all packages.
|
global_update_successful: Successfully updated all packages.
|
||||||
|
|
|
@ -0,0 +1,193 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of Flarum.
|
||||||
|
*
|
||||||
|
* For detailed copyright and license information, please view the
|
||||||
|
* LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Flarum\ExtensionManager\Api\Resource;
|
||||||
|
|
||||||
|
use Flarum\Api\Endpoint;
|
||||||
|
use Flarum\Api\Resource\AbstractResource;
|
||||||
|
use Flarum\Api\Resource\Contracts\Countable;
|
||||||
|
use Flarum\Api\Resource\Contracts\Listable;
|
||||||
|
use Flarum\Api\Resource\Contracts\Paginatable;
|
||||||
|
use Flarum\Api\Schema;
|
||||||
|
use Flarum\ExtensionManager\Api\Schema\SortColumn;
|
||||||
|
use Flarum\ExtensionManager\Exception\CannotFetchExternalExtension;
|
||||||
|
use Flarum\ExtensionManager\External\Extension;
|
||||||
|
use Flarum\ExtensionManager\External\RequestWrapper;
|
||||||
|
use Flarum\Foundation\Application;
|
||||||
|
use GuzzleHttp\Client;
|
||||||
|
use GuzzleHttp\Exception\GuzzleException;
|
||||||
|
use Illuminate\Contracts\Cache\Repository;
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use Tobyz\JsonApiServer\Context;
|
||||||
|
use Tobyz\JsonApiServer\Pagination\OffsetPagination;
|
||||||
|
use Tobyz\JsonApiServer\Schema\CustomFilter;
|
||||||
|
|
||||||
|
class ExternalExtensionResource extends AbstractResource implements Listable, Paginatable, Countable
|
||||||
|
{
|
||||||
|
protected int|null $totalResults = null;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
protected Repository $cache,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function type(): string
|
||||||
|
{
|
||||||
|
return 'external-extensions';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function endpoints(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
Endpoint\Index::make()
|
||||||
|
->authenticated()
|
||||||
|
->admin()
|
||||||
|
->paginate(12, 20),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function fields(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
Schema\Str::make('extensionId')
|
||||||
|
->get(fn (Extension $extension) => $extension->extensionId()),
|
||||||
|
Schema\Str::make('name'),
|
||||||
|
Schema\Str::make('title'),
|
||||||
|
Schema\Str::make('description'),
|
||||||
|
Schema\Str::make('iconUrl')
|
||||||
|
->property('icon_url'),
|
||||||
|
Schema\Arr::make('icon'),
|
||||||
|
Schema\Str::make('highestVersion')
|
||||||
|
->property('highest_version'),
|
||||||
|
Schema\Str::make('httpUri')
|
||||||
|
->property('http_uri'),
|
||||||
|
Schema\Str::make('discussUri')
|
||||||
|
->property('discuss_uri'),
|
||||||
|
Schema\Str::make('vendor'),
|
||||||
|
Schema\Boolean::make('isPremium')
|
||||||
|
->property('is_premium'),
|
||||||
|
Schema\Boolean::make('isLocale')
|
||||||
|
->property('is_locale'),
|
||||||
|
Schema\Str::make('locale'),
|
||||||
|
Schema\Str::make('latestFlarumVersionSupported')
|
||||||
|
->property('latest_flarum_version_supported'),
|
||||||
|
Schema\Boolean::make('compatibleWithLatestFlarum')
|
||||||
|
->property('compatible_with_latest_flarum'),
|
||||||
|
Schema\Integer::make('downloads'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function sorts(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
SortColumn::make('createdAt'),
|
||||||
|
SortColumn::make('downloads'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function filters(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
CustomFilter::make('type', function (object $query, ?string $value) {
|
||||||
|
if ($value) {
|
||||||
|
/** @var RequestWrapper $query */
|
||||||
|
$query->withQueryParams([
|
||||||
|
'filter' => [
|
||||||
|
'type' => $value,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
CustomFilter::make('is', function (object $query, null|string|array $value) {
|
||||||
|
if ($value) {
|
||||||
|
/** @var RequestWrapper $query */
|
||||||
|
$query->withQueryParams([
|
||||||
|
'filter' => [
|
||||||
|
'is' => (array) $value,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
CustomFilter::make('q', function (object $query, ?string $value) {
|
||||||
|
if ($value) {
|
||||||
|
/** @var RequestWrapper $query */
|
||||||
|
$query->withQueryParams([
|
||||||
|
'filter' => [
|
||||||
|
'q' => $value,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function query(Context $context): object
|
||||||
|
{
|
||||||
|
return (new RequestWrapper($this->cache, 'https://flarum.org/api/extensions', 'GET', [
|
||||||
|
'Accept' => 'application/json',
|
||||||
|
]))->withQueryParams([
|
||||||
|
'filter' => [
|
||||||
|
'compatible-with' => Application::VERSION,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function paginate(object $query, OffsetPagination $pagination): void
|
||||||
|
{
|
||||||
|
/** @var RequestWrapper $query */
|
||||||
|
$query->withQueryParams([
|
||||||
|
'page' => [
|
||||||
|
'offset' => $pagination->offset,
|
||||||
|
'limit' => $pagination->limit,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function results(object $query, Context $context): iterable
|
||||||
|
{
|
||||||
|
/** @var RequestWrapper $query */
|
||||||
|
$json = $query->cache(function (RequestWrapper $query) {
|
||||||
|
try {
|
||||||
|
$response = (new Client())->send($query->getRequest());
|
||||||
|
} catch (GuzzleException) {
|
||||||
|
throw new CannotFetchExternalExtension();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($response->getStatusCode() < 200 || $response->getStatusCode() >= 300) {
|
||||||
|
throw new CannotFetchExternalExtension();
|
||||||
|
}
|
||||||
|
|
||||||
|
return json_decode($response->getBody()->getContents(), true);
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->totalResults = $json['meta']['page']['total'] ?? null;
|
||||||
|
|
||||||
|
return (new Collection($json['data']))
|
||||||
|
->map(function (array $data) {
|
||||||
|
$attributes = $data['attributes'];
|
||||||
|
|
||||||
|
$attributes = array_combine(
|
||||||
|
array_map(fn ($key) => Str::snake(Str::camel($key)), array_keys($attributes)),
|
||||||
|
array_values($attributes)
|
||||||
|
);
|
||||||
|
|
||||||
|
return new Extension(array_merge([
|
||||||
|
'id' => $data['id'],
|
||||||
|
], $attributes));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function count(object $query, Context $context): ?int
|
||||||
|
{
|
||||||
|
return $this->totalResults;
|
||||||
|
}
|
||||||
|
}
|
|
@ -19,7 +19,7 @@ class TaskResource extends AbstractDatabaseResource
|
||||||
{
|
{
|
||||||
public function type(): string
|
public function type(): string
|
||||||
{
|
{
|
||||||
return 'package-manager-tasks';
|
return 'extension-manager-tasks';
|
||||||
}
|
}
|
||||||
|
|
||||||
public function model(): string
|
public function model(): string
|
||||||
|
|
30
extensions/package-manager/src/Api/Schema/SortColumn.php
Normal file
30
extensions/package-manager/src/Api/Schema/SortColumn.php
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of Flarum.
|
||||||
|
*
|
||||||
|
* For detailed copyright and license information, please view the
|
||||||
|
* LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Flarum\ExtensionManager\Api\Schema;
|
||||||
|
|
||||||
|
use Flarum\ExtensionManager\External\RequestWrapper;
|
||||||
|
use Tobyz\JsonApiServer\Context;
|
||||||
|
use Tobyz\JsonApiServer\Schema\Sort;
|
||||||
|
|
||||||
|
class SortColumn extends Sort
|
||||||
|
{
|
||||||
|
public static function make(string $name): static
|
||||||
|
{
|
||||||
|
return new static($name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function apply(object $query, string $direction, Context $context): void
|
||||||
|
{
|
||||||
|
/** @var RequestWrapper $query */
|
||||||
|
$query->withQueryParams([
|
||||||
|
'sort' => $direction === 'desc' ? "-$this->name" : $this->name,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
|
@ -9,6 +9,7 @@
|
||||||
|
|
||||||
namespace Flarum\ExtensionManager\Command;
|
namespace Flarum\ExtensionManager\Command;
|
||||||
|
|
||||||
|
use Flarum\Extension\Extension;
|
||||||
use Flarum\Extension\ExtensionManager;
|
use Flarum\Extension\ExtensionManager;
|
||||||
use Flarum\ExtensionManager\Composer\ComposerAdapter;
|
use Flarum\ExtensionManager\Composer\ComposerAdapter;
|
||||||
use Flarum\ExtensionManager\Composer\ComposerJson;
|
use Flarum\ExtensionManager\Composer\ComposerJson;
|
||||||
|
@ -70,7 +71,7 @@ class CheckForUpdatesHandler
|
||||||
|
|
||||||
foreach ($installed as $mainPackageUpdate) {
|
foreach ($installed as $mainPackageUpdate) {
|
||||||
// Skip if not an extension
|
// Skip if not an extension
|
||||||
if (! $this->extensions->getExtension(Util::nameToId($mainPackageUpdate['name']))) {
|
if (! $this->extensions->getExtension(Extension::nameToId($mainPackageUpdate['name']))) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,13 +9,13 @@
|
||||||
|
|
||||||
namespace Flarum\ExtensionManager\Command;
|
namespace Flarum\ExtensionManager\Command;
|
||||||
|
|
||||||
|
use Flarum\Extension\Extension;
|
||||||
use Flarum\Extension\ExtensionManager;
|
use Flarum\Extension\ExtensionManager;
|
||||||
use Flarum\ExtensionManager\Composer\ComposerAdapter;
|
use Flarum\ExtensionManager\Composer\ComposerAdapter;
|
||||||
use Flarum\ExtensionManager\Exception\ComposerRequireFailedException;
|
use Flarum\ExtensionManager\Exception\ComposerRequireFailedException;
|
||||||
use Flarum\ExtensionManager\Exception\ExtensionAlreadyInstalledException;
|
use Flarum\ExtensionManager\Exception\ExtensionAlreadyInstalledException;
|
||||||
use Flarum\ExtensionManager\Extension\Event\Installed;
|
use Flarum\ExtensionManager\Extension\Event\Installed;
|
||||||
use Flarum\ExtensionManager\RequirePackageValidator;
|
use Flarum\ExtensionManager\RequirePackageValidator;
|
||||||
use Flarum\ExtensionManager\Support\Util;
|
|
||||||
use Illuminate\Contracts\Events\Dispatcher;
|
use Illuminate\Contracts\Events\Dispatcher;
|
||||||
use Symfony\Component\Console\Input\StringInput;
|
use Symfony\Component\Console\Input\StringInput;
|
||||||
|
|
||||||
|
@ -39,7 +39,7 @@ class RequireExtensionHandler
|
||||||
|
|
||||||
$this->validator->assertValid(['package' => $command->package]);
|
$this->validator->assertValid(['package' => $command->package]);
|
||||||
|
|
||||||
$extensionId = Util::nameToId($command->package);
|
$extensionId = Extension::nameToId($command->package);
|
||||||
$extension = $this->extensions->getExtension($extensionId);
|
$extension = $this->extensions->getExtension($extensionId);
|
||||||
|
|
||||||
if (! empty($extension)) {
|
if (! empty($extension)) {
|
||||||
|
|
|
@ -9,8 +9,8 @@
|
||||||
|
|
||||||
namespace Flarum\ExtensionManager\Composer;
|
namespace Flarum\ExtensionManager\Composer;
|
||||||
|
|
||||||
|
use Flarum\Extension\Extension;
|
||||||
use Flarum\Extension\ExtensionManager;
|
use Flarum\Extension\ExtensionManager;
|
||||||
use Flarum\ExtensionManager\Support\Util;
|
|
||||||
use Flarum\Foundation\Paths;
|
use Flarum\Foundation\Paths;
|
||||||
use Illuminate\Filesystem\Filesystem;
|
use Illuminate\Filesystem\Filesystem;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
|
@ -39,7 +39,7 @@ class ComposerJson
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only extensions can all be set to * versioning.
|
// Only extensions can all be set to * versioning.
|
||||||
if (! $this->extensions->getExtension(Util::nameToId($packageName))) {
|
if (! $this->extensions->getExtension(Extension::nameToId($packageName))) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of Flarum.
|
||||||
|
*
|
||||||
|
* For detailed copyright and license information, please view the
|
||||||
|
* LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Flarum\ExtensionManager\Exception;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
|
use Flarum\Foundation\KnownError;
|
||||||
|
|
||||||
|
class CannotFetchExternalExtension extends Exception implements KnownError
|
||||||
|
{
|
||||||
|
public function getType(): string
|
||||||
|
{
|
||||||
|
return 'cannot_fetch_external_extension';
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,21 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
/*
|
|
||||||
* This file is part of Flarum.
|
|
||||||
*
|
|
||||||
* For detailed copyright and license information, please view the
|
|
||||||
* LICENSE file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Flarum\PackageManager\Extension;
|
|
||||||
|
|
||||||
class ExtensionUtils
|
|
||||||
{
|
|
||||||
public static function nameToId(string $name): string
|
|
||||||
{
|
|
||||||
[$vendor, $package] = explode('/', $name);
|
|
||||||
$package = str_replace(['flarum-ext-', 'flarum-'], '', $package);
|
|
||||||
|
|
||||||
return "$vendor-$package";
|
|
||||||
}
|
|
||||||
}
|
|
102
extensions/package-manager/src/External/Extension.php
vendored
Normal file
102
extensions/package-manager/src/External/Extension.php
vendored
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of Flarum.
|
||||||
|
*
|
||||||
|
* For detailed copyright and license information, please view the
|
||||||
|
* LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Flarum\ExtensionManager\External;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @property string $name
|
||||||
|
* @property string $title
|
||||||
|
* @property string $description
|
||||||
|
* @property string $icon_url
|
||||||
|
* @property array $icon
|
||||||
|
* @property string $license
|
||||||
|
* @property string $highest_version
|
||||||
|
* @property string $http_uri
|
||||||
|
* @property string $discuss_uri
|
||||||
|
* @property string $vendor
|
||||||
|
* @property bool $is_premium
|
||||||
|
* @property bool $is_locale
|
||||||
|
* @property string $locale
|
||||||
|
* @property string $latest_flarum_version_supported
|
||||||
|
* @property bool $compatible_with_latest_flarum
|
||||||
|
* @property bool $listed_privately
|
||||||
|
* @property int $downloads
|
||||||
|
*/
|
||||||
|
class Extension
|
||||||
|
{
|
||||||
|
protected array $attributes = [];
|
||||||
|
|
||||||
|
public function __construct(array $attributes = [])
|
||||||
|
{
|
||||||
|
$this->attributes = $attributes;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function casts(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'is_premium' => 'bool',
|
||||||
|
'is_locale' => 'bool',
|
||||||
|
'compatible_with_latest_flarum' => 'bool',
|
||||||
|
'listed_privately' => 'bool',
|
||||||
|
'downloads' => 'int',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function extensionId(): string
|
||||||
|
{
|
||||||
|
return \Flarum\Extension\Extension::nameToId($this->name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getAttribute(string $key): mixed
|
||||||
|
{
|
||||||
|
if (array_key_exists($key, $this->attributes)) {
|
||||||
|
return $this->castAttribute($key, $this->attributes[$key]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setAttribute(string $key, mixed $value): void
|
||||||
|
{
|
||||||
|
$this->attributes[$key] = $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function castAttribute(string $key, mixed $value): mixed
|
||||||
|
{
|
||||||
|
if (array_key_exists($key, $this->casts())) {
|
||||||
|
$cast = $this->casts()[$key];
|
||||||
|
|
||||||
|
if (is_string($cast) && function_exists($func = $cast.'val')) {
|
||||||
|
return $func($value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function __get(string $key): mixed
|
||||||
|
{
|
||||||
|
return $this->getAttribute($key);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function __set(string $key, mixed $value): void
|
||||||
|
{
|
||||||
|
$this->setAttribute($key, $value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function __isset(string $key): bool
|
||||||
|
{
|
||||||
|
return isset($this->attributes[$key]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function __unset(string $key): void
|
||||||
|
{
|
||||||
|
unset($this->attributes[$key]);
|
||||||
|
}
|
||||||
|
}
|
73
extensions/package-manager/src/External/RequestWrapper.php
vendored
Normal file
73
extensions/package-manager/src/External/RequestWrapper.php
vendored
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of Flarum.
|
||||||
|
*
|
||||||
|
* For detailed copyright and license information, please view the
|
||||||
|
* LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Flarum\ExtensionManager\External;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use Illuminate\Contracts\Cache\Repository;
|
||||||
|
use Laminas\Diactoros\Request;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @mixin Request
|
||||||
|
*/
|
||||||
|
class RequestWrapper
|
||||||
|
{
|
||||||
|
protected Request $request;
|
||||||
|
protected array $queryParams = [];
|
||||||
|
|
||||||
|
protected static int $ttl = 300; // 5 minutes
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
protected Repository $cache,
|
||||||
|
string $uri,
|
||||||
|
string $method,
|
||||||
|
array $headers = [],
|
||||||
|
) {
|
||||||
|
$this->request = new Request($uri, $method, 'php://temp', $headers);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function withQueryParams(array $queryParams): static
|
||||||
|
{
|
||||||
|
$this->queryParams = array_merge_recursive($this->queryParams, $queryParams);
|
||||||
|
|
||||||
|
$newUri = $this->request->getUri()->withQuery(http_build_query($this->queryParams));
|
||||||
|
$new = $this->request->withUri($newUri);
|
||||||
|
$this->request = $new;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function __call(string $name, array $arguments): static
|
||||||
|
{
|
||||||
|
$new = $this->request->$name(...$arguments);
|
||||||
|
$this->request = $new;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getRequest(): Request
|
||||||
|
{
|
||||||
|
return $this->request;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function cacheKey(): string
|
||||||
|
{
|
||||||
|
return md5($this->request->getUri()->__toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function cache(Closure $callback): array
|
||||||
|
{
|
||||||
|
// We will not cache if there is a search query (filter[q]) in the request.
|
||||||
|
if (isset($this->queryParams['filter']['q'])) {
|
||||||
|
return $callback($this);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->cache->remember($this->cacheKey(), static::$ttl, fn () => $callback($this));
|
||||||
|
}
|
||||||
|
}
|
|
@ -15,14 +15,6 @@ use Symfony\Component\Console\Input\InputInterface;
|
||||||
|
|
||||||
class Util
|
class Util
|
||||||
{
|
{
|
||||||
public static function nameToId(string $name): string
|
|
||||||
{
|
|
||||||
[$vendor, $package] = explode('/', $name);
|
|
||||||
$package = str_replace(['flarum-ext-', 'flarum-'], '', $package);
|
|
||||||
|
|
||||||
return "$vendor-$package";
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function isMajorUpdate(string $currentVersion, string $latestVersion): bool
|
public static function isMajorUpdate(string $currentVersion, string $latestVersion): bool
|
||||||
{
|
{
|
||||||
// Drop any v prefixes
|
// Drop any v prefixes
|
||||||
|
|
|
@ -9,9 +9,9 @@
|
||||||
|
|
||||||
namespace Flarum\ExtensionManager\Tests\integration;
|
namespace Flarum\ExtensionManager\Tests\integration;
|
||||||
|
|
||||||
|
use Flarum\Extension\Extension;
|
||||||
use Flarum\ExtensionManager\Composer\ComposerAdapter;
|
use Flarum\ExtensionManager\Composer\ComposerAdapter;
|
||||||
use Flarum\ExtensionManager\Composer\ComposerJson;
|
use Flarum\ExtensionManager\Composer\ComposerJson;
|
||||||
use Flarum\ExtensionManager\Support\Util;
|
|
||||||
use Flarum\Foundation\Paths;
|
use Flarum\Foundation\Paths;
|
||||||
use Flarum\Testing\integration\RetrievesAuthorizedUsers;
|
use Flarum\Testing\integration\RetrievesAuthorizedUsers;
|
||||||
use Illuminate\Support\Arr;
|
use Illuminate\Support\Arr;
|
||||||
|
@ -45,7 +45,7 @@ class TestCase extends \Flarum\Testing\integration\TestCase
|
||||||
return $package['type'] === 'flarum-extension';
|
return $package['type'] === 'flarum-extension';
|
||||||
});
|
});
|
||||||
$installedExtensionIds = array_map(function (string $name) {
|
$installedExtensionIds = array_map(function (string $name) {
|
||||||
return Util::nameToId($name);
|
return Extension::nameToId($name);
|
||||||
}, Arr::pluck($installedExtensions, 'name'));
|
}, Arr::pluck($installedExtensions, 'name'));
|
||||||
|
|
||||||
if ($exists) {
|
if ($exists) {
|
||||||
|
|
|
@ -59,7 +59,7 @@ class ListTest extends TestCase
|
||||||
$data = json_decode($response->getBody()->getContents(), true)['data'];
|
$data = json_decode($response->getBody()->getContents(), true)['data'];
|
||||||
|
|
||||||
$ids = Arr::pluck($data, 'id');
|
$ids = Arr::pluck($data, 'id');
|
||||||
$this->assertEquals(['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14'], $ids);
|
$this->assertEqualsCanonicalizing(['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14'], $ids);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[Test]
|
#[Test]
|
||||||
|
|
|
@ -16,13 +16,14 @@ import MailPage from './components/MailPage';
|
||||||
import AdvancedPage from './components/AdvancedPage';
|
import AdvancedPage from './components/AdvancedPage';
|
||||||
import PermissionsPage from './components/PermissionsPage';
|
import PermissionsPage from './components/PermissionsPage';
|
||||||
|
|
||||||
export type Extension = {
|
export interface Extension {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
version: string;
|
version: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
icon?: {
|
icon?: {
|
||||||
name: string;
|
name: string;
|
||||||
|
[key: string]: string;
|
||||||
};
|
};
|
||||||
links: {
|
links: {
|
||||||
authors?: {
|
authors?: {
|
||||||
|
@ -44,7 +45,7 @@ export type Extension = {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
require?: Record<string, string>;
|
require?: Record<string, string>;
|
||||||
};
|
}
|
||||||
|
|
||||||
export enum DatabaseDriver {
|
export enum DatabaseDriver {
|
||||||
MySQL = 'MySQL',
|
MySQL = 'MySQL',
|
||||||
|
|
|
@ -19,6 +19,7 @@ import CreateUserModal from './CreateUserModal';
|
||||||
import Icon from '../../common/components/Icon';
|
import Icon from '../../common/components/Icon';
|
||||||
import Input from '../../common/components/Input';
|
import Input from '../../common/components/Input';
|
||||||
import GambitsAutocompleteDropdown from '../../common/components/GambitsAutocompleteDropdown';
|
import GambitsAutocompleteDropdown from '../../common/components/GambitsAutocompleteDropdown';
|
||||||
|
import Pagination from '../../common/components/Pagination';
|
||||||
|
|
||||||
type ColumnData = {
|
type ColumnData = {
|
||||||
/**
|
/**
|
||||||
|
@ -78,11 +79,6 @@ export default class UserListPage extends AdminPage {
|
||||||
*/
|
*/
|
||||||
private pageData: User[] | undefined = undefined;
|
private pageData: User[] | undefined = undefined;
|
||||||
|
|
||||||
/**
|
|
||||||
* Are there more users available?
|
|
||||||
*/
|
|
||||||
private moreData: boolean = false;
|
|
||||||
|
|
||||||
private isLoadingPage: boolean = false;
|
private isLoadingPage: boolean = false;
|
||||||
|
|
||||||
oninit(vnode: Mithril.Vnode<IPageAttrs, this>) {
|
oninit(vnode: Mithril.Vnode<IPageAttrs, this>) {
|
||||||
|
@ -160,76 +156,13 @@ export default class UserListPage extends AdminPage {
|
||||||
{/* Loading spinner that shows when a new page is being loaded */}
|
{/* Loading spinner that shows when a new page is being loaded */}
|
||||||
{this.isLoadingPage && <LoadingIndicator size="large" />}
|
{this.isLoadingPage && <LoadingIndicator size="large" />}
|
||||||
</section>,
|
</section>,
|
||||||
<nav className="UserListPage-gridPagination">
|
<Pagination
|
||||||
<Button
|
currentPage={this.pageNumber + 1}
|
||||||
disabled={this.pageNumber === 0}
|
loadingPageNumber={this.loadingPageNumber + 1}
|
||||||
title={app.translator.trans('core.admin.users.pagination.first_page_button')}
|
total={this.userCount}
|
||||||
onclick={this.goToPage.bind(this, 1)}
|
perPage={this.numPerPage}
|
||||||
icon="fas fa-step-backward"
|
onChange={this.goToPage.bind(this)}
|
||||||
className="Button Button--icon UserListPage-firstPageBtn"
|
/>,
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
disabled={this.pageNumber === 0}
|
|
||||||
title={app.translator.trans('core.admin.users.pagination.back_button')}
|
|
||||||
onclick={this.previousPage.bind(this)}
|
|
||||||
icon="fas fa-chevron-left"
|
|
||||||
className="Button Button--icon UserListPage-backBtn"
|
|
||||||
/>
|
|
||||||
<span className="UserListPage-pageNumber">
|
|
||||||
{app.translator.trans('core.admin.users.pagination.page_counter', {
|
|
||||||
// https://technology.blog.gov.uk/2020/02/24/why-the-gov-uk-design-system-team-changed-the-input-type-for-numbers/
|
|
||||||
current: (
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
inputmode="numeric"
|
|
||||||
pattern="[0-9]*"
|
|
||||||
value={this.loadingPageNumber + 1}
|
|
||||||
aria-label={extractText(app.translator.trans('core.admin.users.pagination.go_to_page_textbox_a11y_label'))}
|
|
||||||
autocomplete="off"
|
|
||||||
className="FormControl UserListPage-pageNumberInput"
|
|
||||||
onchange={(e: InputEvent) => {
|
|
||||||
const target = e.target as HTMLInputElement;
|
|
||||||
let pageNumber = parseInt(target.value);
|
|
||||||
|
|
||||||
if (isNaN(pageNumber)) {
|
|
||||||
// Invalid value, reset to current page
|
|
||||||
target.value = (this.pageNumber + 1).toString();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pageNumber < 1) {
|
|
||||||
// Lower constraint
|
|
||||||
pageNumber = 1;
|
|
||||||
} else if (pageNumber > this.getTotalPageCount()) {
|
|
||||||
// Upper constraint
|
|
||||||
pageNumber = this.getTotalPageCount();
|
|
||||||
}
|
|
||||||
|
|
||||||
target.value = pageNumber.toString();
|
|
||||||
|
|
||||||
this.goToPage(pageNumber);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
currentNum: this.pageNumber + 1,
|
|
||||||
total: this.getTotalPageCount(),
|
|
||||||
})}
|
|
||||||
</span>
|
|
||||||
<Button
|
|
||||||
disabled={!this.moreData}
|
|
||||||
title={app.translator.trans('core.admin.users.pagination.next_button')}
|
|
||||||
onclick={this.nextPage.bind(this)}
|
|
||||||
icon="fas fa-chevron-right"
|
|
||||||
className="Button Button--icon UserListPage-nextBtn"
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
disabled={!this.moreData}
|
|
||||||
title={app.translator.trans('core.admin.users.pagination.last_page_button')}
|
|
||||||
onclick={this.goToPage.bind(this, this.getTotalPageCount())}
|
|
||||||
icon="fas fa-step-forward"
|
|
||||||
className="Button Button--icon UserListPage-lastPageBtn"
|
|
||||||
/>
|
|
||||||
</nav>,
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -482,9 +415,6 @@ export default class UserListPage extends AdminPage {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.then((apiData) => {
|
.then((apiData) => {
|
||||||
// Next link won't be present if there's no more data
|
|
||||||
this.moreData = !!apiData.payload?.links?.next;
|
|
||||||
|
|
||||||
let data = apiData;
|
let data = apiData;
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
@ -509,16 +439,6 @@ export default class UserListPage extends AdminPage {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
nextPage() {
|
|
||||||
this.isLoadingPage = true;
|
|
||||||
this.loadPage(this.pageNumber + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
previousPage() {
|
|
||||||
this.isLoadingPage = true;
|
|
||||||
this.loadPage(this.pageNumber - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param page The **1-based** page number
|
* @param page The **1-based** page number
|
||||||
*/
|
*/
|
||||||
|
@ -532,6 +452,7 @@ export default class UserListPage extends AdminPage {
|
||||||
const params = new URLSearchParams(search?.[1] ?? '');
|
const params = new URLSearchParams(search?.[1] ?? '');
|
||||||
|
|
||||||
params.set('page', `${pageNumber}`);
|
params.set('page', `${pageNumber}`);
|
||||||
window.location.hash = search?.[0] + '?' + params.toString();
|
// window.location.hash = search?.[0] + '?' + params.toString();
|
||||||
|
window.history.replaceState(null, '', search?.[0] + '?' + params.toString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,11 @@ import Model, { ModelData, SavedModelData } from './Model';
|
||||||
import GambitManager from './GambitManager';
|
import GambitManager from './GambitManager';
|
||||||
|
|
||||||
export interface MetaInformation {
|
export interface MetaInformation {
|
||||||
|
page?: {
|
||||||
|
limit?: number;
|
||||||
|
offset?: number;
|
||||||
|
total?: number;
|
||||||
|
};
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -31,6 +31,7 @@ import './utils/patchMithril';
|
||||||
import './utils/classList';
|
import './utils/classList';
|
||||||
import './utils/extractText';
|
import './utils/extractText';
|
||||||
import './utils/formatNumber';
|
import './utils/formatNumber';
|
||||||
|
import './utils/formatAmount';
|
||||||
import './utils/mapRoutes';
|
import './utils/mapRoutes';
|
||||||
import './utils/withAttr';
|
import './utils/withAttr';
|
||||||
import './utils/focusTrap';
|
import './utils/focusTrap';
|
||||||
|
|
|
@ -14,7 +14,6 @@ export interface IInputAttrs extends ComponentAttrs {
|
||||||
clearable?: boolean;
|
clearable?: boolean;
|
||||||
clearLabel?: string;
|
clearLabel?: string;
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
inputClassName?: string;
|
|
||||||
onchange?: (value: string) => void;
|
onchange?: (value: string) => void;
|
||||||
value?: string;
|
value?: string;
|
||||||
stream?: Stream<string>;
|
stream?: Stream<string>;
|
||||||
|
|
94
framework/core/js/src/common/components/Pagination.tsx
Normal file
94
framework/core/js/src/common/components/Pagination.tsx
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
import Component, { ComponentAttrs } from '../Component';
|
||||||
|
import Button from './Button';
|
||||||
|
import app from '../../admin/app';
|
||||||
|
import extractText from '../utils/extractText';
|
||||||
|
|
||||||
|
export interface IPaginationInterface extends ComponentAttrs {
|
||||||
|
total: number;
|
||||||
|
perPage: number;
|
||||||
|
currentPage: number;
|
||||||
|
loadingPageNumber?: number;
|
||||||
|
onChange: (page: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class Pagination<CustomAttrs extends IPaginationInterface = IPaginationInterface> extends Component<CustomAttrs> {
|
||||||
|
view() {
|
||||||
|
const { total, perPage, currentPage, loadingPageNumber, onChange } = this.attrs;
|
||||||
|
|
||||||
|
const totalPageCount = Math.ceil(total / perPage);
|
||||||
|
const moreData = totalPageCount > currentPage;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<nav className="Pagination">
|
||||||
|
<Button
|
||||||
|
disabled={currentPage === 1}
|
||||||
|
title={app.translator.trans('core.admin.users.pagination.first_page_button')}
|
||||||
|
onclick={() => onChange(1)}
|
||||||
|
icon="fas fa-step-backward"
|
||||||
|
className="Button Button--icon Pagination-first"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
disabled={currentPage === 1}
|
||||||
|
title={app.translator.trans('core.admin.users.pagination.back_button')}
|
||||||
|
onclick={() => onChange(currentPage - 1)}
|
||||||
|
icon="fas fa-chevron-left"
|
||||||
|
className="Button Button--icon Pagination-back"
|
||||||
|
/>
|
||||||
|
<span className="Pagination-pageNumber">
|
||||||
|
{app.translator.trans('core.admin.users.pagination.page_counter', {
|
||||||
|
// https://technology.blog.gov.uk/2020/02/24/why-the-gov-uk-design-system-team-changed-the-input-type-for-numbers/
|
||||||
|
current: (
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
inputmode="numeric"
|
||||||
|
pattern="[0-9]*"
|
||||||
|
value={loadingPageNumber ?? currentPage}
|
||||||
|
aria-label={extractText(app.translator.trans('core.admin.users.pagination.go_to_page_textbox_a11y_label'))}
|
||||||
|
autocomplete="off"
|
||||||
|
className="FormControl Pagination-input"
|
||||||
|
onchange={(e: InputEvent) => {
|
||||||
|
const target = e.target as HTMLInputElement;
|
||||||
|
let pageNumber = parseInt(target.value);
|
||||||
|
|
||||||
|
if (isNaN(pageNumber)) {
|
||||||
|
// Invalid value, reset to current page
|
||||||
|
target.value = (currentPage + 1).toString();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pageNumber < 1) {
|
||||||
|
// Lower constraint
|
||||||
|
pageNumber = 1;
|
||||||
|
} else if (pageNumber > totalPageCount) {
|
||||||
|
// Upper constraint
|
||||||
|
pageNumber = totalPageCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
target.value = pageNumber.toString();
|
||||||
|
|
||||||
|
onChange(pageNumber);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
currentNum: currentPage,
|
||||||
|
total: totalPageCount,
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
<Button
|
||||||
|
disabled={!moreData}
|
||||||
|
title={app.translator.trans('core.admin.users.pagination.next_button')}
|
||||||
|
onclick={() => onChange(currentPage + 1)}
|
||||||
|
icon="fas fa-chevron-right"
|
||||||
|
className="Button Button--icon Pagination-next"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
disabled={!moreData}
|
||||||
|
title={app.translator.trans('core.admin.users.pagination.last_page_button')}
|
||||||
|
onclick={() => onChange(totalPageCount)}
|
||||||
|
icon="fas fa-step-forward"
|
||||||
|
className="Button Button--icon Pagination-last"
|
||||||
|
/>
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -126,9 +126,9 @@ export default class SearchModal<CustomAttrs extends ISearchModalAttrs = ISearch
|
||||||
|
|
||||||
tabs(): JSX.Element {
|
tabs(): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<div className="SearchModal-tabs">
|
<div className="Tabs">
|
||||||
<div className="SearchModal-tabs-nav">{this.tabItems().toArray()}</div>
|
<div className="Tabs-nav">{this.tabItems().toArray()}</div>
|
||||||
<div className="SearchModal-tabs-content">{this.activeTabItems().toArray()}</div>
|
<div className="Tabs-content SearchModal-tabs-content">{this.activeTabItems().toArray()}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -47,6 +47,7 @@ export default abstract class PaginatedListState<T extends Model, P extends Pagi
|
||||||
|
|
||||||
protected location!: PaginationLocation;
|
protected location!: PaginationLocation;
|
||||||
public pageSize: number | null;
|
public pageSize: number | null;
|
||||||
|
public totalItems: number | null = null;
|
||||||
|
|
||||||
protected pages: Page<T>[] = [];
|
protected pages: Page<T>[] = [];
|
||||||
protected params: P = {} as P;
|
protected params: P = {} as P;
|
||||||
|
@ -54,6 +55,7 @@ export default abstract class PaginatedListState<T extends Model, P extends Pagi
|
||||||
protected initialLoading: boolean = false;
|
protected initialLoading: boolean = false;
|
||||||
protected loadingPrev: boolean = false;
|
protected loadingPrev: boolean = false;
|
||||||
protected loadingNext: boolean = false;
|
protected loadingNext: boolean = false;
|
||||||
|
protected loadingPage: boolean = false;
|
||||||
|
|
||||||
protected constructor(params: P = {} as P, page: number = 1, pageSize: number | null = null) {
|
protected constructor(params: P = {} as P, page: number = 1, pageSize: number | null = null) {
|
||||||
this.params = params;
|
this.params = params;
|
||||||
|
@ -139,12 +141,19 @@ export default abstract class PaginatedListState<T extends Model, P extends Pagi
|
||||||
}
|
}
|
||||||
|
|
||||||
return app.store.find<T[]>(this.type, params).then((results) => {
|
return app.store.find<T[]>(this.type, params).then((results) => {
|
||||||
|
const usedPerPage = results.payload?.meta?.perPage;
|
||||||
|
const usedTotal = results.payload?.meta?.page?.total;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* If this state does not rely on a preloaded API document to know the page size,
|
* If this state does not rely on a preloaded API document to know the page size,
|
||||||
* then there is no initial list, and therefore the page size can be taken from subsequent requests.
|
* then there is no initial list, and therefore the page size can be taken from subsequent requests.
|
||||||
*/
|
*/
|
||||||
if (!this.pageSize) {
|
if (!this.pageSize || (usedPerPage && this.pageSize !== usedPerPage)) {
|
||||||
this.pageSize = results.payload?.meta?.perPage || PaginatedListState.DEFAULT_PAGE_SIZE;
|
this.pageSize = usedPerPage || PaginatedListState.DEFAULT_PAGE_SIZE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.totalItems || (usedTotal && this.totalItems !== usedTotal)) {
|
||||||
|
this.totalItems = usedTotal || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return results;
|
return results;
|
||||||
|
@ -187,14 +196,25 @@ export default abstract class PaginatedListState<T extends Model, P extends Pagi
|
||||||
|
|
||||||
this.clear();
|
this.clear();
|
||||||
|
|
||||||
|
return this.goto(page);
|
||||||
|
}
|
||||||
|
|
||||||
|
public goto(page: number): Promise<void> {
|
||||||
this.location = { page };
|
this.location = { page };
|
||||||
|
|
||||||
return this.loadPage()
|
if (!this.initialLoading) {
|
||||||
|
this.loadingPage = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.loadPage(page)
|
||||||
.then((results) => {
|
.then((results) => {
|
||||||
this.pages = [];
|
this.pages = [];
|
||||||
this.parseResults(this.location.page, results);
|
this.parseResults(this.location.page, results);
|
||||||
})
|
})
|
||||||
.finally(() => (this.initialLoading = false));
|
.finally(() => {
|
||||||
|
this.initialLoading = false;
|
||||||
|
this.loadingPage = false;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public getPages(): Page<T>[] {
|
public getPages(): Page<T>[] {
|
||||||
|
@ -205,7 +225,7 @@ export default abstract class PaginatedListState<T extends Model, P extends Pagi
|
||||||
}
|
}
|
||||||
|
|
||||||
public isLoading(): boolean {
|
public isLoading(): boolean {
|
||||||
return this.initialLoading || this.loadingNext || this.loadingPrev;
|
return this.initialLoading || this.loadingNext || this.loadingPrev || this.loadingPage;
|
||||||
}
|
}
|
||||||
public isInitialLoading(): boolean {
|
public isInitialLoading(): boolean {
|
||||||
return this.initialLoading;
|
return this.initialLoading;
|
||||||
|
@ -322,18 +342,23 @@ export default abstract class PaginatedListState<T extends Model, P extends Pagi
|
||||||
}
|
}
|
||||||
|
|
||||||
changeSort(sort: string) {
|
changeSort(sort: string) {
|
||||||
let currentSort: string | undefined;
|
|
||||||
|
|
||||||
if (sort === Object.keys(this.sortMap())[0]) {
|
|
||||||
currentSort = undefined;
|
|
||||||
} else {
|
|
||||||
currentSort = sort;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.refreshParams(
|
this.refreshParams(
|
||||||
{
|
{
|
||||||
...this.params,
|
...this.params,
|
||||||
sort: currentSort,
|
sort: sort,
|
||||||
|
},
|
||||||
|
1
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
changeFilter(key: string, value: string) {
|
||||||
|
this.refreshParams(
|
||||||
|
{
|
||||||
|
...this.params,
|
||||||
|
filter: {
|
||||||
|
...this.params.filter,
|
||||||
|
[key]: value,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
1
|
1
|
||||||
);
|
);
|
||||||
|
|
13
framework/core/js/src/common/utils/formatAmount.ts
Normal file
13
framework/core/js/src/common/utils/formatAmount.ts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
export default function formatAmount(size: number): string {
|
||||||
|
const units = ['K', 'M', 'B'];
|
||||||
|
|
||||||
|
for (let i = units.length - 1; i >= 0; i--) {
|
||||||
|
const decimal = Math.pow(1000, i + 1);
|
||||||
|
|
||||||
|
if (size >= decimal) {
|
||||||
|
return (size / decimal).toFixed(1).replace(/\.0$/, '') + units[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return size.toString();
|
||||||
|
}
|
|
@ -1,4 +1,6 @@
|
||||||
.ExtensionPage {
|
.ExtensionPage {
|
||||||
|
padding-bottom: 30px;
|
||||||
|
|
||||||
&-header {
|
&-header {
|
||||||
.helpText {
|
.helpText {
|
||||||
margin-bottom: 5px;
|
margin-bottom: 5px;
|
||||||
|
@ -133,7 +135,7 @@
|
||||||
|
|
||||||
&-body {
|
&-body {
|
||||||
.InfoTile {
|
.InfoTile {
|
||||||
margin-top: 4rem
|
padding: 4rem 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,24 +36,7 @@
|
||||||
|
|
||||||
// Table refreshing overlay
|
// Table refreshing overlay
|
||||||
&--loadingPage {
|
&--loadingPage {
|
||||||
&::after {
|
.loading-container();
|
||||||
content: "";
|
|
||||||
display: block;
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
background: rgba(128, 128, 128, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.LoadingIndicator-container {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&--loaded,
|
&--loaded,
|
||||||
|
@ -88,26 +71,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&-gridPagination {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: auto auto 1fr auto auto;
|
|
||||||
gap: 8px;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
margin-top: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-pageNumber {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-pageNumberInput {
|
|
||||||
display: inline-block;
|
|
||||||
margin: 0 8px;
|
|
||||||
width: auto;
|
|
||||||
max-width: 80px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handles styling of default UserList columns
|
// Handles styling of default UserList columns
|
||||||
|
|
|
@ -74,3 +74,6 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
.Alert-container {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
|
@ -17,6 +17,10 @@
|
||||||
&, .Badge-icon {
|
&, .Badge-icon {
|
||||||
font-size: calc(~"0.56 * var(--size)");
|
font-size: calc(~"0.56 * var(--size)");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&-icon {
|
||||||
|
line-height: normal;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.Badge--size(@size) {
|
.Badge--size(@size) {
|
||||||
|
@ -51,3 +55,20 @@
|
||||||
.Badge--hidden {
|
.Badge--hidden {
|
||||||
--badge-bg: var(--badge-hidden-bg);
|
--badge-bg: var(--badge-hidden-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.Badge--flat {
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Badge--square {
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
.Badge--danger {
|
||||||
|
--badge-color: var(--control-danger-color);
|
||||||
|
--badge-bg: var(--control-danger-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.Badge--success {
|
||||||
|
--badge-bg: var(--success-color);
|
||||||
|
}
|
||||||
|
|
|
@ -88,6 +88,12 @@
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.Form-group--danger {
|
||||||
|
border: 2px solid var(--alert-error-bg);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
.FieldSet-items {
|
.FieldSet-items {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
gap: 5px;
|
gap: 5px;
|
||||||
|
|
|
@ -41,6 +41,10 @@
|
||||||
@media @phone {
|
@media @phone {
|
||||||
font-size: 16px; // minimum font-size required to prevent page zoom on focus in iOS 10
|
font-size: 16px; // minimum font-size required to prevent page zoom on focus in iOS 10
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&-alt {
|
||||||
|
background-color: var(--body-bg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.StackedFormControl {
|
.StackedFormControl {
|
||||||
|
|
19
framework/core/less/common/Pagination.less
Normal file
19
framework/core/less/common/Pagination.less
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
.Pagination {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto auto 1fr auto auto;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 16px;
|
||||||
|
|
||||||
|
&-pageNumber {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-input {
|
||||||
|
display: inline-block;
|
||||||
|
margin: 0 8px;
|
||||||
|
width: auto;
|
||||||
|
max-width: 80px;
|
||||||
|
}
|
||||||
|
}
|
|
@ -47,43 +47,15 @@
|
||||||
row-gap: 6px;
|
row-gap: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&-tabs {
|
&-tabs-content {
|
||||||
&-nav + .Modal-divider {
|
.Dropdown--expanded();
|
||||||
margin-top: 0;
|
|
||||||
|
.Dropdown-header {
|
||||||
|
color: var(--muted-more-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
&-nav {
|
> .SearchModal-section:first-of-type .Modal-divider {
|
||||||
margin-bottom: -1px;
|
margin-top: -1px;
|
||||||
display: flex;
|
|
||||||
column-gap: 4px;
|
|
||||||
padding: 0 14px;
|
|
||||||
|
|
||||||
> .Button {
|
|
||||||
border-radius: 0;
|
|
||||||
font-size: 15px;
|
|
||||||
padding: 12px 8px;
|
|
||||||
border-bottom: 2px solid;
|
|
||||||
border-color: transparent;
|
|
||||||
|
|
||||||
&[active] {
|
|
||||||
--button-color: var(--text-color);
|
|
||||||
--link-color: var(--text-color);
|
|
||||||
border-color: var(--primary-color);
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&-content {
|
|
||||||
.Dropdown--expanded();
|
|
||||||
|
|
||||||
.Dropdown-header {
|
|
||||||
color: var(--muted-more-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
> .SearchModal-section:first-of-type .Modal-divider {
|
|
||||||
margin-top: -1px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
32
framework/core/less/common/Tabs.less
Normal file
32
framework/core/less/common/Tabs.less
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
.Tabs {
|
||||||
|
&-nav + .Modal-divider {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-nav {
|
||||||
|
margin-bottom: -1px;
|
||||||
|
display: flex;
|
||||||
|
column-gap: 4px;
|
||||||
|
padding: 0 14px;
|
||||||
|
|
||||||
|
> .Button {
|
||||||
|
border-radius: 0;
|
||||||
|
font-size: 15px;
|
||||||
|
padding: 12px 8px;
|
||||||
|
border-bottom: 2px solid;
|
||||||
|
border-color: transparent;
|
||||||
|
|
||||||
|
&[active] {
|
||||||
|
--button-color: var(--text-color);
|
||||||
|
--link-color: var(--text-color);
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-divider {
|
||||||
|
margin-top: -1px;
|
||||||
|
border-width: 1px;
|
||||||
|
}
|
||||||
|
}
|
|
@ -25,11 +25,13 @@
|
||||||
@import "LoadingIndicator";
|
@import "LoadingIndicator";
|
||||||
@import "Modal";
|
@import "Modal";
|
||||||
@import "Navigation";
|
@import "Navigation";
|
||||||
|
@import "Pagination";
|
||||||
@import "Pill";
|
@import "Pill";
|
||||||
@import "Placeholder";
|
@import "Placeholder";
|
||||||
@import "Search";
|
@import "Search";
|
||||||
@import "Select";
|
@import "Select";
|
||||||
@import "Table";
|
@import "Table";
|
||||||
|
@import "Tabs";
|
||||||
@import "TextEditor";
|
@import "TextEditor";
|
||||||
@import "ThemeMode";
|
@import "ThemeMode";
|
||||||
@import "Tooltip";
|
@import "Tooltip";
|
||||||
|
|
|
@ -73,6 +73,29 @@ p {
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.loading-container {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
background: rgba(128, 128, 128, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.LoadingIndicator-container {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
mark {
|
mark {
|
||||||
background: var(--highlight-color);
|
background: var(--highlight-color);
|
||||||
padding: 1px;
|
padding: 1px;
|
||||||
|
|
|
@ -143,11 +143,17 @@
|
||||||
@screen-desktop-max: (@screen-desktop-hd - 0.02);
|
@screen-desktop-max: (@screen-desktop-hd - 0.02);
|
||||||
|
|
||||||
@screen-desktop-hd: 1100px;
|
@screen-desktop-hd: 1100px;
|
||||||
|
@screen-desktop-xl: 1600px;
|
||||||
|
@screen-desktop-xxl: 2000px;
|
||||||
|
@screen-desktop-xxxl: 3000px;
|
||||||
|
|
||||||
@phone: ~"(max-width: @{screen-phone-max})";
|
@phone: ~"(max-width: @{screen-phone-max})";
|
||||||
@tablet: ~"(min-width: @{screen-tablet}) and (max-width: @{screen-tablet-max})";
|
@tablet: ~"(min-width: @{screen-tablet}) and (max-width: @{screen-tablet-max})";
|
||||||
@desktop: ~"(min-width: @{screen-desktop}) and (max-width: @{screen-desktop-max})";
|
@desktop: ~"(min-width: @{screen-desktop}) and (max-width: @{screen-desktop-max})";
|
||||||
@desktop-hd: ~"(min-width: @{screen-desktop-hd})";
|
@desktop-hd: ~"(min-width: @{screen-desktop-hd})";
|
||||||
|
@desktop-xl: ~"(min-width: @{screen-desktop-xl})";
|
||||||
|
@desktop-xxl: ~"(min-width: @{screen-desktop-xxl})";
|
||||||
|
@desktop-xxxl: ~"(min-width: @{screen-desktop-xxxl})";
|
||||||
|
|
||||||
@tablet-up: ~"(min-width: @{screen-tablet})";
|
@tablet-up: ~"(min-width: @{screen-tablet})";
|
||||||
@desktop-up: ~"(min-width: @{screen-desktop})";
|
@desktop-up: ~"(min-width: @{screen-desktop})";
|
||||||
|
|
|
@ -15,6 +15,7 @@ use Flarum\Api\Endpoint\Concerns\ExtractsListingParams;
|
||||||
use Flarum\Api\Endpoint\Concerns\HasAuthorization;
|
use Flarum\Api\Endpoint\Concerns\HasAuthorization;
|
||||||
use Flarum\Api\Endpoint\Concerns\HasCustomHooks;
|
use Flarum\Api\Endpoint\Concerns\HasCustomHooks;
|
||||||
use Flarum\Api\Endpoint\Concerns\IncludesData;
|
use Flarum\Api\Endpoint\Concerns\IncludesData;
|
||||||
|
use Flarum\Api\Resource\AbstractDatabaseResource;
|
||||||
use Flarum\Api\Resource\AbstractResource;
|
use Flarum\Api\Resource\AbstractResource;
|
||||||
use Flarum\Api\Resource\Contracts\Countable;
|
use Flarum\Api\Resource\Contracts\Countable;
|
||||||
use Flarum\Api\Resource\Contracts\Listable;
|
use Flarum\Api\Resource\Contracts\Listable;
|
||||||
|
@ -31,7 +32,6 @@ use Tobyz\JsonApiServer\Pagination\OffsetPagination;
|
||||||
use Tobyz\JsonApiServer\Pagination\Pagination;
|
use Tobyz\JsonApiServer\Pagination\Pagination;
|
||||||
use Tobyz\JsonApiServer\Schema\Concerns\HasMeta;
|
use Tobyz\JsonApiServer\Schema\Concerns\HasMeta;
|
||||||
|
|
||||||
use function Tobyz\JsonApiServer\apply_filters;
|
|
||||||
use function Tobyz\JsonApiServer\json_api_response;
|
use function Tobyz\JsonApiServer\json_api_response;
|
||||||
use function Tobyz\JsonApiServer\parse_sort_string;
|
use function Tobyz\JsonApiServer\parse_sort_string;
|
||||||
|
|
||||||
|
@ -70,10 +70,12 @@ class Index extends Endpoint
|
||||||
{
|
{
|
||||||
$this->route('GET', '/')
|
$this->route('GET', '/')
|
||||||
->query(function ($query, ?Pagination $pagination, Context $context): Context {
|
->query(function ($query, ?Pagination $pagination, Context $context): Context {
|
||||||
|
$collection = $context->collection;
|
||||||
|
|
||||||
// This model has a searcher API, so we'll use that instead of the default.
|
// This model has a searcher API, so we'll use that instead of the default.
|
||||||
// The searcher API allows swapping the default search engine for a custom one.
|
// The searcher API allows swapping the default search engine for a custom one.
|
||||||
$search = $context->api->getContainer()->make(SearchManager::class);
|
$search = $context->api->getContainer()->make(SearchManager::class);
|
||||||
$modelClass = $query->getModel()::class;
|
$modelClass = $collection instanceof AbstractDatabaseResource ? $collection->model() : null;
|
||||||
|
|
||||||
if ($query instanceof Builder && $search->searchable($modelClass)) {
|
if ($query instanceof Builder && $search->searchable($modelClass)) {
|
||||||
$actor = $context->getActor();
|
$actor = $context->getActor();
|
||||||
|
@ -147,6 +149,8 @@ class Index extends Endpoint
|
||||||
|
|
||||||
$meta = $this->serializeMeta($context);
|
$meta = $this->serializeMeta($context);
|
||||||
|
|
||||||
|
$models = $collection->results($query, $context);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
$collection instanceof Countable &&
|
$collection instanceof Countable &&
|
||||||
! is_null($total = $collection->count($query, $context))
|
! is_null($total = $collection->count($query, $context))
|
||||||
|
@ -154,8 +158,6 @@ class Index extends Endpoint
|
||||||
$meta['page']['total'] = $total;
|
$meta['page']['total'] = $total;
|
||||||
}
|
}
|
||||||
|
|
||||||
$models = $collection->results($query, $context);
|
|
||||||
|
|
||||||
$models = $this->callAfterHook($context, $models);
|
$models = $this->callAfterHook($context, $models);
|
||||||
|
|
||||||
$total ??= null;
|
$total ??= null;
|
||||||
|
@ -245,14 +247,28 @@ class Index extends Endpoint
|
||||||
|
|
||||||
$collection = $context->collection;
|
$collection = $context->collection;
|
||||||
|
|
||||||
if (! $collection instanceof \Tobyz\JsonApiServer\Resource\Listable) {
|
if (! $collection instanceof Listable) {
|
||||||
throw new RuntimeException(
|
throw new RuntimeException(
|
||||||
sprintf('%s must implement %s', $collection::class, \Tobyz\JsonApiServer\Resource\Listable::class),
|
sprintf('%s must implement %s', $collection::class, \Tobyz\JsonApiServer\Resource\Listable::class),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
apply_filters($query, $filters, $collection, $context);
|
$context = $context->withCollection($collection);
|
||||||
|
$availableFilters = $collection->filters();
|
||||||
|
|
||||||
|
foreach ($filters as $name => $value) {
|
||||||
|
foreach ($availableFilters as $filter) {
|
||||||
|
if ($filter->name === $name && $filter->isVisible($context)) {
|
||||||
|
$filter->apply($query, $value, $context);
|
||||||
|
continue 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw (new BadRequestException("Invalid filter: $name"))->setSource([
|
||||||
|
'parameter' => "[$name]",
|
||||||
|
]);
|
||||||
|
}
|
||||||
} catch (Sourceable $e) {
|
} catch (Sourceable $e) {
|
||||||
throw $e->prependSource(['parameter' => 'filter']);
|
throw $e->prependSource(['parameter' => 'filter']);
|
||||||
}
|
}
|
||||||
|
|
|
@ -330,7 +330,7 @@ abstract class AbstractDatabaseResource extends AbstractResource implements
|
||||||
return new ($this->model());
|
return new ($this->model());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function filters(): array
|
final public function filters(): array
|
||||||
{
|
{
|
||||||
throw new RuntimeException('Not supported in Flarum, please use a model searcher instead https://docs.flarum.org/extend/search.');
|
throw new RuntimeException('Not supported in Flarum, please use a model searcher instead https://docs.flarum.org/extend/search.');
|
||||||
}
|
}
|
||||||
|
|
|
@ -84,7 +84,7 @@ class Extension implements Arrayable
|
||||||
$this->assignId();
|
$this->assignId();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected static function nameToId(string $name): string
|
public static function nameToId(string $name): string
|
||||||
{
|
{
|
||||||
[$vendor, $package] = explode('/', $name);
|
[$vendor, $package] = explode('/', $name);
|
||||||
$package = str_replace(['flarum-ext-', 'flarum-'], '', $package);
|
$package = str_replace(['flarum-ext-', 'flarum-'], '', $package);
|
||||||
|
|
Loading…
Reference in New Issue
Block a user