chore(package-manager): last tweaks before beta tag

chore: fix workflow errors
chore: fix workflow errors
chore: avoid updating an extension that wasn't directly required
chore: prevent job overlap
chore: reorganize code, separate state from view
fix: update checking ui display
chore: minor improvements

Signed-off-by: Sami Mazouz <ilyasmazouz@gmail.com>
This commit is contained in:
Sami Mazouz 2022-08-20 09:53:23 +01:00
parent 082117d8bc
commit 335c602cea
18 changed files with 429 additions and 281 deletions

View File

@ -41,7 +41,8 @@ return [
$paths = resolve(Paths::class);
$document->payload['flarum-package-manager.writable_dirs'] = is_writable($paths->vendor)
&& is_writable($paths->storage.'/.composer')
&& is_writable($paths->storage)
&& (! file_exists($paths->storage.'/.composer') || is_writable($paths->storage.'/.composer'))
&& is_writable($paths->base.'/composer.json')
&& is_writable($paths->base.'/composer.lock');

View File

@ -1,11 +1,17 @@
import app from 'flarum/admin/app';
import Component from 'flarum/common/Component';
import Alert from 'flarum/common/components/Alert';
import { ComponentAttrs } from 'flarum/common/Component';
import Installer from './Installer';
import Updater from './Updater';
import Mithril from 'mithril';
export default class ControlSection extends Component<ComponentAttrs> {
oninit(vnode: Mithril.Vnode<ComponentAttrs, this>) {
super.oninit(vnode);
}
export default class ControlSection extends Component {
view() {
return (
<div className="ExtensionPage-permissions PackageManager-controlSection">

View File

@ -7,7 +7,7 @@ import Tooltip from 'flarum/common/components/Tooltip';
import Button from 'flarum/common/components/Button';
import { Extension } from 'flarum/admin/AdminApplication';
import { UpdatedPackage } from './Updater';
import { UpdatedPackage } from '../states/ControlSectionState';
import WhyNotModal from './WhyNotModal';
import Label from './Label';
@ -40,7 +40,7 @@ export default class ExtensionItem<Attrs extends ExtensionItemAttrs = ExtensionI
<div className="PackageManager-extension-info">
<div className="PackageManager-extension-name">{extension.extra['flarum-extension'].title}</div>
<div className="PackageManager-extension-version">
<span className="PackageManager-extension-version-current">{this.version(extension.version)}</span>
<span className="PackageManager-extension-version-current">{this.version(updates['version'])}</span>
{latestVersion ? (
<Label className="PackageManager-extension-version-latest" type={updates['latest-minor'] ? 'success' : 'warning'}>
{this.version(latestVersion)}

View File

@ -9,11 +9,12 @@ import errorHandler from '../utils/errorHandler';
import jumpToQueue from '../utils/jumpToQueue';
import { AsyncBackendResponse } from '../shims';
interface InstallerAttrs extends ComponentAttrs {}
export interface InstallerAttrs extends ComponentAttrs {}
export type InstallerLoadingTypes = 'extension-install' | null;
export default class Installer extends Component<InstallerAttrs> {
packageName!: Stream<string>;
isLoading: boolean = false;
oninit(vnode: Mithril.Vnode<InstallerAttrs, this>): void {
super.oninit(vnode);
@ -32,7 +33,13 @@ export default class Installer extends Component<InstallerAttrs> {
</p>
<div className="FormControl-container">
<input className="FormControl" id="install-extension" placeholder="vendor/package-name" bidi={this.packageName} />
<Button className="Button" icon="fas fa-download" onclick={this.onsubmit.bind(this)} loading={this.isLoading}>
<Button
className="Button"
icon="fas fa-download"
onclick={this.onsubmit.bind(this)}
loading={app.packageManager.control.isLoading('extension-install')}
disabled={app.packageManager.control.isLoadingOtherThan('extension-install')}
>
{app.translator.trans('flarum-package-manager.admin.extensions.proceed')}
</Button>
</div>
@ -47,7 +54,7 @@ export default class Installer extends Component<InstallerAttrs> {
}
onsubmit(): void {
this.isLoading = true;
app.packageManager.control.setLoading('extension-install');
app.modal.show(LoadingModal);
app
@ -73,7 +80,7 @@ export default class Installer extends Component<InstallerAttrs> {
}
})
.finally(() => {
this.isLoading = false;
app.packageManager.control.setLoading(null);
m.redraw();
});
}

View File

@ -7,20 +7,21 @@ import LoadingModal from 'flarum/admin/components/LoadingModal';
import Alert from 'flarum/common/components/Alert';
import RequestError from 'flarum/common/utils/RequestError';
import { UpdatedPackage, UpdateState } from './Updater';
import { UpdatedPackage, UpdateState } from '../states/ControlSectionState';
import errorHandler from '../utils/errorHandler';
import WhyNotModal from './WhyNotModal';
import ExtensionItem from './ExtensionItem';
import { AsyncBackendResponse } from '../shims';
import jumpToQueue from '../utils/jumpToQueue';
interface MajorUpdaterAttrs extends ComponentAttrs {
export interface MajorUpdaterAttrs extends ComponentAttrs {
coreUpdate: UpdatedPackage;
updateState: UpdateState;
}
export type MajorUpdaterLoadingTypes = 'major-update' | 'major-update-dry-run';
export default class MajorUpdater<T extends MajorUpdaterAttrs = MajorUpdaterAttrs> extends Component<T> {
isLoading: string | null = null;
updateState!: UpdateState;
oninit(vnode: Mithril.Vnode<T, this>) {
@ -29,7 +30,7 @@ export default class MajorUpdater<T extends MajorUpdaterAttrs = MajorUpdaterAttr
this.updateState = this.attrs.updateState;
}
view(vnode: Mithril.Vnode<T, this>): Mithril.Children {
view(): Mithril.Children {
// @todo move Form-group--danger class to core for reuse
return (
<div className="Form-group Form-group--danger PackageManager-majorUpdate">
@ -38,11 +39,21 @@ export default class MajorUpdater<T extends MajorUpdaterAttrs = MajorUpdaterAttr
<p className="helpText">{app.translator.trans('flarum-package-manager.admin.major_updater.description')}</p>
<div className="PackageManager-updaterControls">
<Tooltip text={app.translator.trans('flarum-package-manager.admin.major_updater.dry_run_help')}>
<Button className="Button" icon="fas fa-vial" onclick={this.update.bind(this, true)}>
<Button
className="Button"
icon="fas fa-vial"
onclick={this.update.bind(this, true)}
disabled={app.packageManager.control.isLoadingOtherThan('major-update-dry-run')}
>
{app.translator.trans('flarum-package-manager.admin.major_updater.dry_run')}
</Button>
</Tooltip>
<Button className="Button Button--danger" icon="fas fa-play" onclick={this.update.bind(this, false)}>
<Button
className="Button Button--danger"
icon="fas fa-play"
onclick={this.update.bind(this, false)}
disabled={app.packageManager.control.isLoadingOtherThan('major-update')}
>
{app.translator.trans('flarum-package-manager.admin.major_updater.update')}
</Button>
</div>
@ -83,7 +94,7 @@ export default class MajorUpdater<T extends MajorUpdaterAttrs = MajorUpdaterAttr
}
update(dryRun: boolean) {
this.isLoading = `update-${dryRun ? 'dry-run' : 'run'}`;
app.packageManager.control.setLoading(dryRun ? 'major-update-dry-run' : 'major-update');
app.modal.show(LoadingModal);
app
@ -109,7 +120,7 @@ export default class MajorUpdater<T extends MajorUpdaterAttrs = MajorUpdaterAttr
this.updateState.incompatibleExtensions = e.response?.errors?.pop()?.incompatible_extensions as string[];
})
.finally(() => {
this.isLoading = null;
app.packageManager.control.setLoading(null);
m.redraw();
});
}

View File

@ -24,7 +24,7 @@ export default class QueueSection extends Component<{}> {
oninit(vnode: Mithril.Vnode<{}, this>) {
super.oninit(vnode);
app.packageManagerQueue.load();
app.packageManager.queue.load();
}
view() {
@ -36,7 +36,7 @@ export default class QueueSection extends Component<{}> {
<Button
className="Button Button--icon"
icon="fas fa-sync-alt"
onclick={() => app.packageManagerQueue.load()}
onclick={() => app.packageManager.queue.load()}
aria-label={app.translator.trans('flarum-package-manager.admin.sections.queue.refresh')}
/>
</div>
@ -154,7 +154,7 @@ export default class QueueSection extends Component<{}> {
}
queueTable() {
const tasks = app.packageManagerQueue.getItems();
const tasks = app.packageManager.queue.getItems();
if (!tasks) {
return <LoadingIndicator />;
@ -193,7 +193,7 @@ export default class QueueSection extends Component<{}> {
</tbody>
</table>
<Pagination list={app.packageManagerQueue} />
<Pagination list={app.packageManager.queue} />
</>
);
}

View File

@ -8,16 +8,16 @@ import ControlSection from './ControlSection';
export default class SettingsPage extends ExtensionPage {
sections(vnode: Mithril.VnodeDOM<ExtensionPageAttrs, this>): ItemList<unknown> {
// @todo add core feature to register sections
const items = super.sections(vnode);
if (app.data.settings['flarum-package-manager.queue_jobs']) {
items.add('queue', <QueueSection />, 5);
}
items.setPriority('content', 10);
items.add('control', <ControlSection />, 8);
items.setPriority('content', 10);
if (parseInt(app.data.settings['flarum-package-manager.queue_jobs'])) {
items.add('queue', <QueueSection />, 5);
}
items.setPriority('permissions', 0);
return items;

View File

@ -1,278 +1,126 @@
import Mithril from 'mithril';
import app from 'flarum/admin/app';
import Component, { ComponentAttrs } from 'flarum/common/Component';
import Button from 'flarum/common/components/Button';
import humanTime from 'flarum/common/helpers/humanTime';
import LoadingModal from 'flarum/admin/components/LoadingModal';
import errorHandler from '../utils/errorHandler';
import LoadingIndicator from 'flarum/common/components/LoadingIndicator';
import MajorUpdater from './MajorUpdater';
import ExtensionItem from './ExtensionItem';
import extractText from 'flarum/common/utils/extractText';
import jumpToQueue from '../utils/jumpToQueue';
import { AsyncBackendResponse } from '../shims';
import { Extension } from 'flarum/admin/AdminApplication';
import Alert from 'flarum/common/components/Alert';
import ItemList from '@flarum/core/src/common/utils/ItemList';
export type UpdatedPackage = {
name: string;
version: string;
latest: string;
'latest-minor': string | null;
'latest-major': string | null;
'latest-status': string;
description: string;
};
export interface IUpdaterAttrs extends ComponentAttrs {}
export type ComposerUpdates = {
installed: UpdatedPackage[];
};
export type LastUpdateCheck = {
checkedAt: Date | null;
updates: ComposerUpdates;
};
type UpdateType = 'major' | 'minor' | 'global';
type UpdateStatus = 'success' | 'failure' | null;
export type UpdateState = {
ranAt: Date | null;
status: UpdateStatus;
limitedPackages: string[];
incompatibleExtensions: string[];
};
export type LastUpdateRun = {
[key in UpdateType]: UpdateState;
} & {
limitedPackages: () => string[];
};
interface UpdaterAttrs extends ComponentAttrs {}
export default class Updater extends Component<UpdaterAttrs> {
isLoading: string | null = null;
packageUpdates: Record<string, UpdatedPackage> = {};
lastUpdateCheck: LastUpdateCheck = JSON.parse(app.data.settings['flarum-package-manager.last_update_check']) as LastUpdateCheck;
get lastUpdateRun(): LastUpdateRun {
const lastUpdateRun = JSON.parse(app.data.settings['flarum-package-manager.last_update_run']) as LastUpdateRun;
lastUpdateRun.limitedPackages = () => [
...lastUpdateRun.major.limitedPackages,
...lastUpdateRun.minor.limitedPackages,
...lastUpdateRun.global.limitedPackages,
];
return lastUpdateRun;
}
oninit(vnode: Mithril.Vnode<UpdaterAttrs, this>) {
super.oninit(vnode);
}
export type UpdaterLoadingTypes = 'check' | 'minor-update' | 'global-update' | 'extension-update' | null;
export default class Updater extends Component<IUpdaterAttrs> {
view() {
const extensions = this.getExtensionUpdates();
let coreUpdate: UpdatedPackage | undefined = this.getCoreUpdate();
let core: any;
if (coreUpdate) {
core = {
id: 'flarum-core',
name: 'flarum/core',
version: app.data.settings.version,
icon: {
backgroundImage: `url(${app.forum.attribute('baseUrl')}/assets/extensions/flarum-package-manager/flarum.svg`,
},
extra: {
'flarum-extension': {
title: app.translator.trans('flarum-package-manager.admin.updater.flarum'),
},
},
};
}
const core = app.packageManager.control.coreUpdate;
return [
<div className="Form-group">
<label>{app.translator.trans('flarum-package-manager.admin.updater.updater_title')}</label>
<p className="helpText">{app.translator.trans('flarum-package-manager.admin.updater.updater_help')}</p>
{this.lastUpdateCheck?.checkedAt && (
<p className="PackageManager-lastUpdatedAt">
<span className="PackageManager-lastUpdatedAt-label">
{app.translator.trans('flarum-package-manager.admin.updater.last_update_checked_at')}
</span>
<span className="PackageManager-lastUpdatedAt-value">{humanTime(this.lastUpdateCheck.checkedAt)}</span>
</p>
)}
<div className="PackageManager-updaterControls">
<Button
className="Button"
icon="fas fa-sync-alt"
onclick={this.checkForUpdates.bind(this)}
loading={this.isLoading === 'check'}
disabled={this.isLoading !== null && this.isLoading !== 'check'}
>
{app.translator.trans('flarum-package-manager.admin.updater.check_for_updates')}
</Button>
<Button
className="Button"
icon="fas fa-play"
onclick={this.updateGlobally.bind(this)}
loading={this.isLoading === 'global-update'}
disabled={this.isLoading !== null && this.isLoading !== 'global-update'}
>
{app.translator.trans('flarum-package-manager.admin.updater.run_global_update')}
</Button>
</div>
{this.isLoading !== null ? (
<div className="PackageManager-extensions">
<LoadingIndicator />
</div>
) : extensions.length || core ? (
<div className="PackageManager-extensions">
<div className="PackageManager-extensions-grid">
{core ? (
<ExtensionItem
extension={core}
updates={coreUpdate}
isCore={true}
onClickUpdate={this.updateCoreMinor.bind(this)}
whyNotWarning={this.lastUpdateRun.limitedPackages().includes('flarum/core')}
/>
) : null}
{extensions.map((extension: Extension) => (
<ExtensionItem
extension={extension}
updates={this.packageUpdates[extension.id]}
onClickUpdate={this.updateExtension.bind(this, extension)}
whyNotWarning={this.lastUpdateRun.limitedPackages().includes(extension.name)}
/>
))}
</div>
</div>
) : null}
{this.lastUpdateCheckView()}
<div className="PackageManager-updaterControls">{this.controlItems().toArray()}</div>
{this.availableUpdatesView()}
</div>,
coreUpdate && coreUpdate['latest-major'] ? <MajorUpdater coreUpdate={coreUpdate} updateState={this.lastUpdateRun.major} /> : null,
core && core.package['latest-major'] ? (
<MajorUpdater coreUpdate={core.package} updateState={app.packageManager.control.lastUpdateRun.major} />
) : null,
];
}
getExtensionUpdates(): Extension[] {
this.lastUpdateCheck?.updates?.installed?.filter((composerPackage: UpdatedPackage) => {
const id = composerPackage.name.replace('/', '-').replace(/(flarum-ext-)|(flarum-)/, '');
const extension = app.data.extensions[id];
const safeToUpdate = ['semver-safe-update', 'update-possible'].includes(composerPackage['latest-status']);
if (extension && safeToUpdate) {
this.packageUpdates[extension.id] = composerPackage;
}
return extension && safeToUpdate;
});
return (Object.values(app.data.extensions) as Extension[]).filter((extension: Extension) => this.packageUpdates[extension.id]);
lastUpdateCheckView() {
return (
(app.packageManager.control.lastUpdateCheck?.checkedAt && (
<p className="PackageManager-lastUpdatedAt">
<span className="PackageManager-lastUpdatedAt-label">
{app.translator.trans('flarum-package-manager.admin.updater.last_update_checked_at')}
</span>
<span className="PackageManager-lastUpdatedAt-value">{humanTime(app.packageManager.control.lastUpdateCheck.checkedAt)}</span>
</p>
)) ||
null
);
}
getCoreUpdate(): UpdatedPackage | undefined {
return this.lastUpdateCheck?.updates?.installed?.filter((composerPackage: UpdatedPackage) => composerPackage.name === 'flarum/core').pop();
}
availableUpdatesView() {
const state = app.packageManager.control;
checkForUpdates() {
this.isLoading = 'check';
app
.request<AsyncBackendResponse | LastUpdateCheck>({
method: 'POST',
url: `${app.forum.attribute('apiUrl')}/package-manager/check-for-updates`,
errorHandler,
})
.then((response) => {
if ((response as AsyncBackendResponse).processing) {
jumpToQueue();
} else {
this.lastUpdateCheck = response as LastUpdateCheck;
}
})
.finally(() => {
this.isLoading = null;
m.redraw();
});
}
updateCoreMinor() {
if (confirm(extractText(app.translator.trans('flarum-package-manager.admin.minor_update_confirmation.content')))) {
app.modal.show(LoadingModal);
this.isLoading = 'minor-update';
app
.request<AsyncBackendResponse | null>({
method: 'POST',
url: `${app.forum.attribute('apiUrl')}/package-manager/minor-update`,
errorHandler,
})
.then((response) => {
if (response?.processing) {
jumpToQueue();
} else {
app.alerts.show({ type: 'success' }, app.translator.trans('flarum-package-manager.admin.update_successful'));
window.location.reload();
}
})
.finally(() => {
this.isLoading = null;
m.redraw();
});
if (app.packageManager.control.isLoading()) {
return (
<div className="PackageManager-extensions">
<LoadingIndicator />
</div>
);
}
if (!(state.extensionUpdates.length || state.coreUpdate)) {
return (
<div className="PackageManager-extensions">
<Alert type="success" dismissible={false}>
{app.translator.trans('flarum-package-manager.admin.updater.up_to_date')}
</Alert>
</div>
);
}
return (
<div className="PackageManager-extensions">
<div className="PackageManager-extensions-grid">
{state.coreUpdate ? (
<ExtensionItem
extension={state.coreUpdate.extension}
updates={state.coreUpdate.package}
isCore={true}
onClickUpdate={() => state.updateCoreMinor()}
whyNotWarning={state.lastUpdateRun.limitedPackages().includes('flarum/core')}
/>
) : null}
{state.extensionUpdates.map((extension: Extension) => (
<ExtensionItem
extension={extension}
updates={state.packageUpdates[extension.id]}
onClickUpdate={() => state.updateExtension(extension)}
whyNotWarning={state.lastUpdateRun.limitedPackages().includes(extension.name)}
/>
))}
</div>
</div>
);
}
updateExtension(extension: any) {
app.modal.show(LoadingModal);
this.isLoading = 'extension-update';
controlItems() {
const items = new ItemList();
app
.request<AsyncBackendResponse | null>({
method: 'PATCH',
url: `${app.forum.attribute('apiUrl')}/package-manager/extensions/${extension.id}`,
errorHandler,
})
.then((response) => {
if (response?.processing) {
jumpToQueue();
} else {
app.alerts.show(
{ type: 'success' },
app.translator.trans('flarum-package-manager.admin.extensions.successful_update', {
extension: extension.extra['flarum-extension'].title,
})
);
window.location.reload();
}
})
.finally(() => {
this.isLoading = null;
m.redraw();
});
}
items.add(
'updateCheck',
<Button
className="Button"
icon="fas fa-sync-alt"
onclick={() => app.packageManager.control.checkForUpdates()}
loading={app.packageManager.control.isLoading('check')}
disabled={app.packageManager.control.isLoadingOtherThan('check')}
>
{app.translator.trans('flarum-package-manager.admin.updater.check_for_updates')}
</Button>,
100
);
updateGlobally() {
app.modal.show(LoadingModal);
this.isLoading = 'global-update';
items.add(
'globalUpdate',
<Button
className="Button"
icon="fas fa-play"
onclick={() => app.packageManager.control.updateGlobally()}
loading={app.packageManager.control.isLoading('global-update')}
disabled={app.packageManager.control.isLoadingOtherThan('global-update')}
>
{app.translator.trans('flarum-package-manager.admin.updater.run_global_update')}
</Button>
);
app
.request<AsyncBackendResponse | null>({
method: 'POST',
url: `${app.forum.attribute('apiUrl')}/package-manager/global-update`,
errorHandler,
})
.then((response) => {
if (response?.processing) {
jumpToQueue();
} else {
app.alerts.show({ type: 'success' }, app.translator.trans('flarum-package-manager.admin.updater.global_update_successful'));
window.location.reload();
}
})
.finally(() => {
this.isLoading = null;
m.redraw();
});
return items;
}
}

View File

@ -8,14 +8,14 @@ import SettingsPage from './components/SettingsPage';
import Task from './models/Task';
import jumpToQueue from './utils/jumpToQueue';
import QueueState from './states/QueueState';
import extractText from 'flarum/common/utils/extractText';
import { AsyncBackendResponse } from './shims';
import PackageManagerState from './states/PackageManagerState';
app.initializers.add('flarum-package-manager', (app) => {
app.store.models['package-manager-tasks'] = Task;
app.packageManagerQueue = new QueueState();
app.packageManager = new PackageManagerState();
app.extensionData
.for('flarum-package-manager')

View File

@ -1,4 +1,4 @@
import QueueState from './states/QueueState';
import PackageManagerState from './states/PackageManagerState';
export interface AsyncBackendResponse {
processing: boolean;
@ -6,6 +6,6 @@ export interface AsyncBackendResponse {
declare module 'flarum/admin/AdminApplication' {
export default interface AdminApplication {
packageManagerQueue: QueueState;
packageManager: PackageManagerState;
}
}

View File

@ -0,0 +1,239 @@
import app from 'flarum/admin/app';
import LoadingModal from 'flarum/admin/components/LoadingModal';
import { UpdaterLoadingTypes } from '../components/Updater';
import { InstallerLoadingTypes } from '../components/Installer';
import { MajorUpdaterLoadingTypes } from '../components/MajorUpdater';
import { AsyncBackendResponse } from '../shims';
import errorHandler from '../utils/errorHandler';
import jumpToQueue from '../utils/jumpToQueue';
import { Extension } from 'flarum/admin/AdminApplication';
import extractText from 'flarum/common/utils/extractText';
export type UpdatedPackage = {
name: string;
version: string;
latest: string;
'latest-minor': string | null;
'latest-major': string | null;
'latest-status': string;
description: string;
};
export type ComposerUpdates = {
installed: UpdatedPackage[];
};
export type LastUpdateCheck = {
checkedAt: Date | null;
updates: ComposerUpdates;
};
type UpdateType = 'major' | 'minor' | 'global';
type UpdateStatus = 'success' | 'failure' | null;
export type UpdateState = {
ranAt: Date | null;
status: UpdateStatus;
limitedPackages: string[];
incompatibleExtensions: string[];
};
export type LastUpdateRun = {
[key in UpdateType]: UpdateState;
} & {
limitedPackages: () => string[];
};
export type LoadingTypes = UpdaterLoadingTypes | InstallerLoadingTypes | MajorUpdaterLoadingTypes;
export type CoreUpdate = {
package: UpdatedPackage;
extension: Extension;
};
export default class ControlSectionState {
loading: LoadingTypes = null;
public packageUpdates: Record<string, UpdatedPackage> = {};
public lastUpdateCheck!: LastUpdateCheck;
public extensionUpdates!: Extension[];
public coreUpdate: CoreUpdate | null = null;
get lastUpdateRun(): LastUpdateRun {
const lastUpdateRun = JSON.parse(app.data.settings['flarum-package-manager.last_update_run']) as LastUpdateRun;
lastUpdateRun.limitedPackages = () => [
...lastUpdateRun.major.limitedPackages,
...lastUpdateRun.minor.limitedPackages,
...lastUpdateRun.global.limitedPackages,
];
return lastUpdateRun;
}
constructor() {
this.lastUpdateCheck = JSON.parse(app.data.settings['flarum-package-manager.last_update_check']) as LastUpdateCheck;
this.extensionUpdates = this.formatExtensionUpdates(this.lastUpdateCheck);
this.coreUpdate = this.formatCoreUpdate(this.lastUpdateCheck);
}
isLoading(name: LoadingTypes = null): boolean {
return (name && this.loading === name) || (!name && this.loading !== null);
}
isLoadingOtherThan(name: LoadingTypes): boolean {
return this.loading !== null && this.loading !== name;
}
setLoading(name: LoadingTypes): void {
this.loading = name;
}
checkForUpdates() {
this.setLoading('check');
app
.request<AsyncBackendResponse | LastUpdateCheck>({
method: 'POST',
url: `${app.forum.attribute('apiUrl')}/package-manager/check-for-updates`,
errorHandler,
})
.then((response) => {
if ((response as AsyncBackendResponse).processing) {
jumpToQueue();
} else {
this.lastUpdateCheck = response as LastUpdateCheck;
this.extensionUpdates = this.formatExtensionUpdates(response as LastUpdateCheck);
this.coreUpdate = this.formatCoreUpdate(response as LastUpdateCheck);
m.redraw();
}
})
.finally(() => {
this.setLoading(null);
m.redraw();
});
}
updateCoreMinor() {
if (confirm(extractText(app.translator.trans('flarum-package-manager.admin.minor_update_confirmation.content')))) {
app.modal.show(LoadingModal);
this.setLoading('minor-update');
app
.request<AsyncBackendResponse | null>({
method: 'POST',
url: `${app.forum.attribute('apiUrl')}/package-manager/minor-update`,
errorHandler,
})
.then((response) => {
if (response?.processing) {
jumpToQueue();
} else {
app.alerts.show({ type: 'success' }, app.translator.trans('flarum-package-manager.admin.update_successful'));
window.location.reload();
}
})
.finally(() => {
this.setLoading(null);
app.modal.close();
m.redraw();
});
}
}
updateExtension(extension: Extension) {
app.modal.show(LoadingModal);
this.setLoading('extension-update');
app
.request<AsyncBackendResponse | null>({
method: 'PATCH',
url: `${app.forum.attribute('apiUrl')}/package-manager/extensions/${extension.id}`,
errorHandler,
})
.then((response) => {
if (response?.processing) {
jumpToQueue();
} else {
app.alerts.show(
{ type: 'success' },
app.translator.trans('flarum-package-manager.admin.extensions.successful_update', {
extension: extension.extra['flarum-extension'].title,
})
);
window.location.reload();
}
})
.finally(() => {
this.setLoading(null);
app.modal.close();
m.redraw();
});
}
updateGlobally() {
app.modal.show(LoadingModal);
this.setLoading('global-update');
app
.request<AsyncBackendResponse | null>({
method: 'POST',
url: `${app.forum.attribute('apiUrl')}/package-manager/global-update`,
errorHandler,
})
.then((response) => {
if (response?.processing) {
jumpToQueue();
} else {
app.alerts.show({ type: 'success' }, app.translator.trans('flarum-package-manager.admin.updater.global_update_successful'));
window.location.reload();
}
})
.finally(() => {
this.setLoading(null);
app.modal.close();
m.redraw();
});
}
formatExtensionUpdates(lastUpdateCheck: LastUpdateCheck): Extension[] {
this.packageUpdates = {};
lastUpdateCheck?.updates?.installed?.filter((composerPackage: UpdatedPackage) => {
const id = composerPackage.name.replace('/', '-').replace(/(flarum-ext-)|(flarum-)/, '');
const extension = app.data.extensions[id];
const safeToUpdate = ['semver-safe-update', 'update-possible'].includes(composerPackage['latest-status']);
if (extension && safeToUpdate) {
this.packageUpdates[extension.id] = composerPackage;
}
return extension && safeToUpdate;
});
return (Object.values(app.data.extensions) as Extension[]).filter((extension: Extension) => this.packageUpdates[extension.id]);
}
formatCoreUpdate(lastUpdateCheck: LastUpdateCheck): CoreUpdate | null {
const core = lastUpdateCheck?.updates?.installed?.filter((composerPackage: UpdatedPackage) => composerPackage.name === 'flarum/core').pop();
if (!core) return null;
return {
package: core,
extension: {
id: 'flarum-core',
name: 'flarum/core',
version: app.data.settings.version,
icon: {
// @ts-ignore
backgroundImage: `url(${app.forum.attribute('baseUrl')}/assets/extensions/flarum-package-manager/flarum.svg`,
},
extra: {
'flarum-extension': {
title: extractText(app.translator.trans('flarum-package-manager.admin.updater.flarum')),
},
},
},
};
}
}

View File

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

View File

@ -4,7 +4,7 @@ import { ApiQueryParamsPlural } from 'flarum/common/Store';
export default class QueueState {
private tasks: Task[] | null = null;
private limit = 5;
private limit = 20;
private offset = 0;
private total = 0;

View File

@ -6,7 +6,7 @@ window.jumpToQueue = jumpToQueue;
export default function jumpToQueue(): void {
app.modal.close();
m.route.set(app.route('extension', { id: 'flarum-package-manager' }));
app.packageManagerQueue.load();
app.packageManager.queue.load();
setTimeout(() => {
document.getElementById('PackageManager-queueSection')?.scrollIntoView({ block: 'nearest' });
}, 200);

View File

@ -11,6 +11,7 @@
flex-wrap: wrap;
gap: 8px;
grid-area: controls;
margin-bottom: 16px;
}
.PackageManager-extensions {
@ -19,7 +20,6 @@
display: grid;
grid-template-columns: repeat(auto-fit, calc(~"100% / 3 - var(--gap)"));
gap: var(--gap);
margin-top: 16px;
}
}

View File

@ -22,7 +22,7 @@ flarum-package-manager:
update: Update
file_permissions: >
The package manager requires read and write permissions on the following files and directories: composer.json, composer.lock, vendor, storage/.composer
The package manager requires read and write permissions on the following files and directories: composer.json, composer.lock, vendor, storage, storage/.composer
major_updater:
description: Major Flarum updates are not backwards compatible, meaning that some of your currently installed extensions, and manually made modifications might not work with this new version.
@ -78,6 +78,7 @@ flarum-package-manager:
Make sure the PHP version used for the queue is {php_version}. Make sure <a href='{folder_perms_link}'>folder permissions</a> are correctly configured.
updater:
up_to_date: Everything is up to date!
check_for_updates: Check for updates
flarum: Flarum Core
global_update_successful: Successfully updated all packages.

View File

@ -10,6 +10,8 @@
namespace Flarum\PackageManager\Command;
use Flarum\Extension\ExtensionManager;
use Flarum\Foundation\Paths;
use Flarum\Foundation\ValidationException;
use Flarum\PackageManager\Composer\ComposerAdapter;
use Flarum\PackageManager\Exception\ComposerUpdateFailedException;
use Flarum\PackageManager\Exception\ExtensionNotInstalledException;
@ -46,18 +48,25 @@ class UpdateExtensionHandler
*/
protected $events;
/**
* @var Paths
*/
protected $paths;
public function __construct(
ComposerAdapter $composer,
ExtensionManager $extensions,
UpdateExtensionValidator $validator,
LastUpdateCheck $lastUpdateCheck,
Dispatcher $events
Dispatcher $events,
Paths $paths
) {
$this->composer = $composer;
$this->extensions = $extensions;
$this->validator = $validator;
$this->lastUpdateCheck = $lastUpdateCheck;
$this->events = $events;
$this->paths = $paths;
}
/**
@ -76,6 +85,17 @@ class UpdateExtensionHandler
throw new ExtensionNotInstalledException($command->extensionId);
}
$rootComposer = json_decode(file_get_contents("{$this->paths->base}/composer.json"), true);
// If this was installed as a requirement for another extension,
// don't update it directly.
// @TODO communicate this in the UI.
if (! isset($rootComposer['require'][$extension->name]) && ! empty($extension->getExtensionDependencyIds())) {
throw new ValidationException([
'message' => "Cannot update $extension->name. It was installed as a requirement for other extensions: ".implode(', ', $extension->getExtensionDependencyIds()).'. Update those extensions instead.'
]);
}
$output = $this->composer->run(
new StringInput("require $extension->name:*"),
$command->task ?? null

View File

@ -12,6 +12,7 @@ namespace Flarum\PackageManager\Job;
use Flarum\Bus\Dispatcher;
use Flarum\PackageManager\Command\BusinessCommandInterface;
use Flarum\Queue\AbstractJob;
use Illuminate\Queue\Middleware\WithoutOverlapping;
use Throwable;
class ComposerCommandJob extends AbstractJob
@ -62,4 +63,11 @@ class ComposerCommandJob extends AbstractJob
$this->fail($exception);
}
public function middleware()
{
return [
new WithoutOverlapping(),
];
}
}