1
0
mirror of https://github.com/flarum/core.git synced 2025-08-23 16:43:21 +02:00

Compare commits

..

4 Commits

Author SHA1 Message Date
Sami Mazouz
8fd6dbc3fd fix: handle message deletion effects 2025-08-22 14:32:03 +01:00
Sami Mazouz
1f09ff1942 fix: delete conversation when no messages are left 2025-08-22 13:57:04 +01:00
flarum-bot
a46ce07255 Bundled output for commit 8e404e4415
Includes transpiled JS/TS, and Typescript declaration files (typings).

[skip ci]
2025-06-01 07:48:18 +00:00
David Sevilla Martin
8e404e4415 fix: prevent <button>'s from becoming form submit when they shouldn't (#4221)
* fix: prevent <button>'s from becoming form submit when they shouldn't

Adds `type=button` to most `<button>` usage (except the AdminPage#submitButton). The first button of type submit (which is the default in HTML if undeclared) becomes what the enter keybind presses in a form. This makes the behavior with these components make more sense.

* Prettier on framework
2025-06-01 08:45:32 +01:00
32 changed files with 107 additions and 24 deletions

2
extensions/emoji/js/dist/forum.js generated vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -70,6 +70,7 @@ export default function addComposerAutocomplete() {
return ( return (
<Tooltip text={name}> <Tooltip text={name}>
<button <button
type="button"
key={emoji} key={emoji}
onclick={() => applySuggestion(emoji)} onclick={() => applySuggestion(emoji)}
onmouseenter={function () { onmouseenter={function () {

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -17,7 +17,7 @@ export default class MentionsDropdownItem<CustomAttrs extends IMentionsDropdownI
const className = classList('MentionsDropdownItem', 'PostPreview', `MentionsDropdown-${mentionable.type()}`); const className = classList('MentionsDropdownItem', 'PostPreview', `MentionsDropdown-${mentionable.type()}`);
return ( return (
<button className={className} {...attrs}> <button className={className} type="button" {...attrs}>
<span className="PostPreview-content">{vnode.children}</span> <span className="PostPreview-content">{vnode.children}</span>
</button> </button>
); );

View File

@@ -15,6 +15,7 @@ export default class PostQuoteButton extends Fragment {
return ( return (
<button <button
className="Button PostQuoteButton" className="Button PostQuoteButton"
type="button"
onclick={() => { onclick={() => {
reply(this.post, this.content); reply(this.post, this.content);
}} }}

View File

@@ -30,6 +30,9 @@ export default class Dialog extends Model {
unreadCount() { unreadCount() {
return Model.attribute<number>('unreadCount').call(this); return Model.attribute<number>('unreadCount').call(this);
} }
lastMessageId() {
return Model.attribute<number>('lastMessageId').call(this);
}
lastReadMessageId() { lastReadMessageId() {
return Model.attribute<number>('lastReadMessageId').call(this); return Model.attribute<number>('lastReadMessageId').call(this);
} }

View File

@@ -59,6 +59,56 @@ const MessageControls = {
return message.delete().then(() => { return message.delete().then(() => {
context.attrs.state.remove(message); context.attrs.state.remove(message);
const dialog = message.dialog();
if (dialog) {
const noMessagesLeft =
context.attrs.state.getAllItems().filter((m) => {
const mDialog = m.dialog();
if (!mDialog) return false;
return mDialog?.id() === dialog!.id();
}).length === 0;
if (noMessagesLeft) {
app.dialogs.remove(dialog!);
m.route.set(app.route('messages'));
}
if (parseInt(message.id()!) === dialog.lastMessageId()) {
const lastMessage = context.attrs.state
.getAllItems()
.filter((m) => {
const mDialog = m.dialog();
if (!mDialog) return false;
return mDialog.id() === dialog?.id();
})
.sort((a, b) => parseInt(a.id()!) - parseInt(b.id()!))
.pop();
if (lastMessage) {
dialog!.pushData({
relationships: {
...dialog!.data.relationships,
lastMessage: {
data: {
type: 'dialog-messages',
id: lastMessage.id()!,
},
},
},
});
dialog.pushAttributes({
lastMessageId: parseInt(lastMessage.id()!),
});
}
}
}
m.redraw(); m.redraw();
}); });
}, },

View File

@@ -68,9 +68,6 @@ class DialogResource extends Resource\AbstractDatabaseResource
$connection = UserDialogState::query()->getConnection(); $connection = UserDialogState::query()->getConnection();
$grammar = UserDialogState::query()->getGrammar(); $grammar = UserDialogState::query()->getGrammar();
$table = $grammar->wrapTable('dialogs');
$column = $grammar->wrap('last_message_id');
UserDialogState::query() UserDialogState::query()
->where('dialog_user.user_id', $context->getActor()->id) ->where('dialog_user.user_id', $context->getActor()->id)
->update([ ->update([
@@ -121,6 +118,7 @@ class DialogResource extends Resource\AbstractDatabaseResource
->get(function (Dialog $dialog) { ->get(function (Dialog $dialog) {
return $dialog->state->last_read_at; return $dialog->state->last_read_at;
}), }),
Schema\Integer::make('lastMessageId'),
Schema\Integer::make('lastReadMessageId') Schema\Integer::make('lastReadMessageId')
->visible(fn (Dialog $dialog) => $dialog->state !== null) ->visible(fn (Dialog $dialog) => $dialog->state !== null)
->get(function (Dialog $dialog) { ->get(function (Dialog $dialog) {

View File

@@ -70,6 +70,24 @@ class DialogMessage extends AbstractModel implements Formattable
->toSql() ->toSql()
.')'); .')');
}); });
static::deleted(function (self $message) {
if ($message->dialog) {
if ($message->dialog->messages()->count() === 0) {
$message->dialog->delete();
} elseif ($message->dialog->first_message_id === $message->id) {
$message->dialog->setFirstMessage(
$message->dialog->messages()->oldest('id')->first()
);
$message->dialog->save();
} elseif ($message->dialog->last_message_id === $message->id) {
$message->dialog->setLastMessage(
$message->dialog->messages()->latest('id')->first()
);
$message->dialog->save();
}
}
});
} }
public function dialog(): BelongsTo public function dialog(): BelongsTo

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -279,6 +279,7 @@ export default class StatisticsWidget extends DashboardWidget {
return ( return (
<button <button
className={classList('Button--ua-reset StatisticsWidget-entity', { active: this.selectedEntity === entity })} className={classList('Button--ua-reset StatisticsWidget-entity', { active: this.selectedEntity === entity })}
type="button"
onclick={this.changeEntity.bind(this, entity)} onclick={this.changeEntity.bind(this, entity)}
> >
<h3 className="StatisticsWidget-heading">{app.translator.trans('flarum-statistics.admin.statistics.' + entity + '_heading')}</h3> <h3 className="StatisticsWidget-heading">{app.translator.trans('flarum-statistics.admin.statistics.' + entity + '_heading')}</h3>

2
framework/core/js/dist/admin.js generated vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

2
framework/core/js/dist/forum.js generated vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -67,7 +67,13 @@ export default abstract class AdminPage<CustomAttrs extends IPageAttrs = IPageAt
*/ */
submitButton(): Mithril.Children { submitButton(): Mithril.Children {
return ( return (
<Button onclick={this.saveSettings.bind(this)} className="Button Button--primary" loading={this.loading} disabled={!this.isChanged()}> <Button
type="submit"
onclick={this.saveSettings.bind(this)}
className="Button Button--primary"
loading={this.loading}
disabled={!this.isChanged()}
>
{app.translator.trans('core.admin.settings.submit_button')} {app.translator.trans('core.admin.settings.submit_button')}
</Button> </Button>
); );

View File

@@ -25,12 +25,12 @@ export default class PermissionsPage extends AdminPage {
.all<Group>('groups') .all<Group>('groups')
.filter((group) => [Group.GUEST_ID, Group.MEMBER_ID].indexOf(group.id()!) === -1) .filter((group) => [Group.GUEST_ID, Group.MEMBER_ID].indexOf(group.id()!) === -1)
.map((group) => ( .map((group) => (
<button className="Button Group" onclick={() => app.modal.show(EditGroupModal, { group })}> <button className="Button Group" type="button" onclick={() => app.modal.show(EditGroupModal, { group })}>
<GroupBadge group={group} className="Group-icon" label={null} /> <GroupBadge group={group} className="Group-icon" label={null} />
<span className="Group-name">{group.namePlural()}</span> <span className="Group-name">{group.namePlural()}</span>
</button> </button>
))} ))}
<button className="Button Group Group--add" onclick={() => app.modal.show(EditGroupModal)}> <button className="Button Group Group--add" type="button" onclick={() => app.modal.show(EditGroupModal)}>
<Icon name="fas fa-plus" className="Group-icon" /> <Icon name="fas fa-plus" className="Group-icon" />
<span className="Group-name">{app.translator.trans('core.admin.permissions.new_group_button')}</span> <span className="Group-name">{app.translator.trans('core.admin.permissions.new_group_button')}</span>
</button> </button>

View File

@@ -352,6 +352,7 @@ export default class UserListPage extends AdminPage {
<button <button
onclick={toggleEmailVisibility} onclick={toggleEmailVisibility}
className="Button Button--text UserList-emailIconBtn" className="Button Button--text UserList-emailIconBtn"
type="button"
title={app.translator.trans('core.admin.users.grid.columns.email.visibility_show')} title={app.translator.trans('core.admin.users.grid.columns.email.visibility_show')}
> >
<Icon name="far fa-eye-slash fa-fw" className="icon" /> <Icon name="far fa-eye-slash fa-fw" className="icon" />

View File

@@ -21,7 +21,7 @@ export default class DetailedDropdownItem<
> extends Component<CustomAttrs> { > extends Component<CustomAttrs> {
view() { view() {
return ( return (
<button className="DetailedDropdownItem hasIcon" onclick={this.attrs.onclick}> <button type="button" className="DetailedDropdownItem hasIcon" onclick={this.attrs.onclick}>
<Icon name={this.attrs.active ? 'fas fa-check' : 'fas'} className="Button-icon" /> <Icon name={this.attrs.active ? 'fas fa-check' : 'fas'} className="Button-icon" />
<span className="DetailedDropdownItem-content"> <span className="DetailedDropdownItem-content">
<Icon name={this.attrs.icon} className="Button-icon" /> <Icon name={this.attrs.icon} className="Button-icon" />

View File

@@ -139,6 +139,7 @@ export default class Dropdown<CustomAttrs extends IDropdownAttrs = IDropdownAttr
getButton(children: Mithril.ChildArray): Mithril.Vnode<any, any> { getButton(children: Mithril.ChildArray): Mithril.Vnode<any, any> {
let button = ( let button = (
<button <button
type="button"
className={'Dropdown-toggle ' + this.attrs.buttonClassName} className={'Dropdown-toggle ' + this.attrs.buttonClassName}
aria-haspopup="menu" aria-haspopup="menu"
aria-label={this.attrs.accessibleToggleLabel} aria-label={this.attrs.accessibleToggleLabel}

View File

@@ -45,6 +45,7 @@ export default class SplitDropdown<CustomAttrs extends ISplitDropdownAttrs = ISp
<> <>
{button} {button}
<button <button
type="button"
className={'Dropdown-toggle Button Button--icon ' + this.attrs.buttonClassName} className={'Dropdown-toggle Button Button--icon ' + this.attrs.buttonClassName}
aria-haspopup="menu" aria-haspopup="menu"
aria-label={this.attrs.accessibleToggleLabel} aria-label={this.attrs.accessibleToggleLabel}

View File

@@ -43,6 +43,7 @@ export default class AvatarEditor extends Component {
<div className={classList(['AvatarEditor', 'Dropdown', this.attrs.className, this.loading && 'loading', this.isDraggedOver && 'dragover'])}> <div className={classList(['AvatarEditor', 'Dropdown', this.attrs.className, this.loading && 'loading', this.isDraggedOver && 'dragover'])}>
<Avatar user={user} loading="eager" /> <Avatar user={user} loading="eager" />
<button <button
type="button"
className={user.avatarUrl() ? 'Dropdown-toggle' : 'Dropdown-toggle AvatarEditor--noAvatar'} className={user.avatarUrl() ? 'Dropdown-toggle' : 'Dropdown-toggle AvatarEditor--noAvatar'}
title={app.translator.trans('core.forum.user.avatar_upload_tooltip')} title={app.translator.trans('core.forum.user.avatar_upload_tooltip')}
ariaLabel={app.translator.trans('core.forum.user.avatar_upload_tooltip')} ariaLabel={app.translator.trans('core.forum.user.avatar_upload_tooltip')}

View File

@@ -40,6 +40,7 @@ export default class PostMeta<CustomAttrs extends IPostMetaAttrs = IPostMetaAttr
items.add( items.add(
'time', 'time',
<button <button
type="button"
className={classList({ className={classList({
'Button Button--text': true, 'Button Button--text': true,
'Dropdown-toggle Button--link': !!permalink, 'Dropdown-toggle Button--link': !!permalink,

View File

@@ -58,7 +58,7 @@ export default class PostStreamScrubber extends Component {
return ( return (
<div className={classNames.join(' ')}> <div className={classNames.join(' ')}>
<button className="Button Dropdown-toggle" data-toggle="dropdown"> <button type="button" className="Button Dropdown-toggle" data-toggle="dropdown">
{viewing} <Icon name={'fas fa-sort'} /> {viewing} <Icon name={'fas fa-sort'} />
</button> </button>

View File

@@ -54,7 +54,7 @@ export default class ReplyPlaceholder<CustomAttrs extends IReplyPlaceholderAttrs
}); });
return ( return (
<button className="Post ReplyPlaceholder" onclick={reply}> <button type="button" className="Post ReplyPlaceholder" onclick={reply}>
<div className="Post-container"> <div className="Post-container">
<div className="Post-side"> <div className="Post-side">
<Avatar user={app.session.user} className="Post-avatar" /> <Avatar user={app.session.user} className="Post-avatar" />