Merge commit 'c4eec632b92c800ae38dba111c4c76e63bb1c0de' into glitch-soc/merge-upstream

This commit is contained in:
Claire 2026-04-22 20:24:21 +02:00
commit b6abf7a69f
30 changed files with 242 additions and 110 deletions

View File

@ -76,6 +76,26 @@
}
.emptyMessage {
padding: 8px 16px;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
gap: 8px;
padding: 16px;
font-size: 13px;
}
.loadingIndicator {
--spinner-size: 20px;
position: relative;
display: block;
width: var(--spinner-size);
height: var(--spinner-size);
overflow: hidden;
& :global(.circular-progress) {
width: var(--spinner-size);
height: var(--spinner-size);
}
}

View File

@ -22,6 +22,8 @@ import { matchWidth } from 'mastodon/components/dropdown/utils';
import { IconButton } from 'mastodon/components/icon_button';
import { useOnClickOutside } from 'mastodon/hooks/useOnClickOutside';
import { LoadingIndicator } from '../loading_indicator';
import classes from './combobox.module.scss';
import { FormFieldWrapper } from './form_field_wrapper';
import type { CommonFieldWrapperProps } from './form_field_wrapper';
@ -531,6 +533,7 @@ const ComboboxWithRef = <Item extends ComboboxItem, GroupKey extends string>(
<div {...props} className={classNames(classes.popover, placement)}>
<StatusMessageWrapper
showStatus={showStatusMessageInMenu}
isLoading={isLoading}
status={statusMessage}
>
{hasGroups ? (
@ -592,10 +595,20 @@ Combobox.displayName = 'Combobox';
const StatusMessageWrapper: React.FC<{
showStatus: boolean;
status: string;
isLoading: boolean;
children: React.ReactNode;
}> = ({ showStatus, status, children }) => {
}> = ({ showStatus, status, isLoading, children }) => {
if (showStatus) {
return <span className={classes.emptyMessage}>{status}</span>;
return (
<span className={classes.emptyMessage}>
{isLoading && (
<span className={classes.loadingIndicator}>
<LoadingIndicator role='none' />
</span>
)}
{status}
</span>
);
}
return children;

View File

@ -1,3 +1,13 @@
.wrapper {
--diameter: 20px;
--padding: 2px;
--transition: 0.2s ease-in-out;
display: inline-block;
position: relative;
height: calc(var(--diameter) + var(--padding) * 2);
}
.input {
position: absolute;
opacity: 0;
@ -9,10 +19,6 @@
}
.toggle {
--diameter: 20px;
--padding: 2px;
--transition: 0.2s ease-in-out;
display: inline-flex;
align-items: center;
border-radius: 9999px;
@ -68,8 +74,3 @@
:global([dir='rtl']) .input:checked + .toggle::before {
transform: translateX(calc(-1 * (var(--diameter) - (var(--padding) * 2))));
}
.wrapper {
display: inline-block;
position: relative;
}

View File

@ -31,20 +31,17 @@ ToggleField.displayName = 'ToggleField';
export const Toggle = forwardRef<HTMLInputElement, Props>(
({ className, size, ...otherProps }, ref) => (
<span className={classes.wrapper}>
<span
className={classes.wrapper}
style={{ '--diameter': size ? `${size}px` : undefined } as CSSProperties}
>
<input
{...otherProps}
type='checkbox'
className={classes.input}
ref={ref}
/>
<span
className={classNames(classes.toggle, className)}
style={
{ '--diameter': size ? `${size}px` : undefined } as CSSProperties
}
hidden
/>
<span className={classNames(classes.toggle, className)} hidden />
</span>
),
);

View File

@ -331,6 +331,10 @@ const TopicField: React.FC = () => {
[topic],
);
const isCurrentTopicOnlySuggestion =
tags.length === 1 && tags[0]?.id === 'new';
const hideTagSuggestions = !tags.length || isCurrentTopicOnlySuggestion;
return (
<ComboboxField
required={false}
@ -369,7 +373,7 @@ const TopicField: React.FC = () => {
}
: undefined
}
suppressMenu={!tags.length}
suppressMenu={hideTagSuggestions}
/>
);
};

View File

@ -7,8 +7,8 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import { NonceProvider } from 'react-select';
import AsyncSelect from 'react-select/async';
import Toggle from 'react-toggle';
import { Toggle } from '@/mastodon/components/form_fields/toggle_field';
import { injectIntl } from '@/mastodon/components/intl';
import SettingToggle from '../../notifications/components/setting_toggle';

View File

@ -7,8 +7,6 @@ import { useParams, useHistory, Link } from 'react-router-dom';
import { isFulfilled } from '@reduxjs/toolkit';
import Toggle from 'react-toggle';
import ChevronRightIcon from '@/material-icons/400-24px/chevron_right.svg?react';
import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react';
import { fetchList } from 'mastodon/actions/lists';
@ -20,7 +18,11 @@ import { Avatar } from 'mastodon/components/avatar';
import { AvatarGroup } from 'mastodon/components/avatar_group';
import { Column } from 'mastodon/components/column';
import { ColumnHeader } from 'mastodon/components/column_header';
import { SelectField, TextInputField } from 'mastodon/components/form_fields';
import {
SelectField,
TextInputField,
Toggle,
} from 'mastodon/components/form_fields';
import { Icon } from 'mastodon/components/icon';
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
import type { List } from 'mastodon/models/list';

View File

@ -1,7 +1,7 @@
import type { PropsWithChildren } from 'react';
import { useCallback } from 'react';
import Toggle from 'react-toggle';
import { Toggle } from '@/mastodon/components/form_fields/toggle_field';
interface Props {
checked: boolean;

View File

@ -3,7 +3,7 @@ import { PureComponent } from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import Toggle from 'react-toggle';
import { Toggle } from '@/mastodon/components/form_fields/toggle_field';
export default class SettingToggle extends PureComponent {
@ -27,7 +27,7 @@ export default class SettingToggle extends PureComponent {
return (
<div className='setting-toggle'>
<Toggle disabled={disabled} id={id} checked={settings.getIn(settingPath, defaultValue)} onChange={this.onChange} onKeyDown={this.onKeyDown} />
<Toggle disabled={disabled} id={id} checked={settings.getIn(settingPath, defaultValue)} onChange={this.onChange} onKeyDown={this.onKeyDown} size={16} />
<label htmlFor={id} className='setting-toggle__label'>{label}</label>
</div>
);

View File

@ -6,8 +6,6 @@ import classNames from 'classnames';
import { Helmet } from 'react-helmet';
import { useHistory } from 'react-router-dom';
import Toggle from 'react-toggle';
import AddPhotoAlternateIcon from '@/material-icons/400-24px/add_photo_alternate.svg?react';
import EditIcon from '@/material-icons/400-24px/edit.svg?react';
import PersonIcon from '@/material-icons/400-24px/person.svg?react';
@ -16,7 +14,11 @@ import { closeOnboarding } from 'mastodon/actions/onboarding';
import { Button } from 'mastodon/components/button';
import { Column } from 'mastodon/components/column';
import { ColumnHeader } from 'mastodon/components/column_header';
import { TextAreaField, TextInputField } from 'mastodon/components/form_fields';
import {
TextAreaField,
TextInputField,
Toggle,
} from 'mastodon/components/form_fields';
import { Icon } from 'mastodon/components/icon';
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
import { me } from 'mastodon/initial_state';

View File

@ -6,8 +6,7 @@ import type { Map } from 'immutable';
import { OrderedSet } from 'immutable';
import { shallowEqual } from 'react-redux';
import Toggle from 'react-toggle';
import { Toggle } from '@/mastodon/components/form_fields/toggle_field';
import { fetchAccount } from 'mastodon/actions/accounts';
import { Button } from 'mastodon/components/button';
import type { Status } from 'mastodon/models/status';

View File

@ -13,12 +13,12 @@
"about.not_available": "Þessar upplýsingar hafa ekki verið gerðar aðgengilegar á þessum netþjóni.",
"about.powered_by": "Dreifhýstur samskiptamiðill keyrður með {mastodon}",
"about.rules": "Reglur netþjónsins",
"account.account_note_header": "Einkaminnispunktur",
"account.account_note_header": "Mínir minnispunktar",
"account.activity": "Virkni",
"account.add_note": "Bæta við einkaminnispunkti",
"account.add_note": "Bæta við persónulegum minnispunkti",
"account.add_or_remove_from_list": "Bæta við eða fjarlægja af listum",
"account.badges.admin": "Stjóri",
"account.badges.blocked": "Lokað á",
"account.badges.blocked": "Útilokað",
"account.badges.bot": "Yrki",
"account.badges.domain_blocked": "Útilokað lén",
"account.badges.group": "Hópur",
@ -28,13 +28,13 @@
"account.block_domain": "Útiloka lénið {domain}",
"account.block_short": "Útiloka",
"account.blocked": "Útilokaður",
"account.blocking": "Útilokun",
"account.blocking": "Útilok",
"account.cancel_follow_request": "Taka fylgjendabeiðni til baka",
"account.copy": "Afrita tengil í notandasnið",
"account.direct": "Einkaspjall við @{name}",
"account.disable_notifications": "Hætta að láta mig vita þegar @{name} sendir inn",
"account.domain_blocking": "Útiloka lén",
"account.edit_note": "Breyta einkaminnispunkti",
"account.domain_blocking": "Útilokað lén",
"account.edit_note": "Breyta persónulegum minnispunkti",
"account.edit_profile": "Breyta notandasniði",
"account.edit_profile_short": "Breyta",
"account.enable_notifications": "Láta mig vita þegar @{name} sendir inn",
@ -55,8 +55,8 @@
"account.filters.replies_toggle": "Birta svör",
"account.follow": "Fylgjast með",
"account.follow_back": "Fylgjast með til baka",
"account.follow_back_short": "Fylgjast með til baka",
"account.follow_request": "Beiðni um að fylgjast með",
"account.follow_back_short": "Fylgjaka",
"account.follow_request": "Beiðni um að fylgja",
"account.follow_request_cancel": "Hætta við beiðni",
"account.follow_request_cancel_short": "Hætta við",
"account.follow_request_short": "Beiðni",
@ -90,7 +90,7 @@
"account.menu.note.description": "Einungis sýnilegt þér",
"account.menu.open_original_page": "Skoða á {domain}",
"account.menu.remove_follower": "Fjarlægja fylgjanda",
"account.menu.report": "Kæra aðgang",
"account.menu.report": "Tilkynna aðgang",
"account.menu.share": "Deila…",
"account.menu.show_reblogs": "Sýna endurbirtingar í tímalínu",
"account.menu.unblock": "Aflétta útilokun aðgangs",
@ -102,7 +102,7 @@
"account.mute_short": "Þagga niður",
"account.muted": "Þaggaður",
"account.muting": "Þöggun",
"account.mutual": "Þið fylgist með hvor öðrum",
"account.mutual": "Þið fylgið hvor öðrum",
"account.name.copy": "Afrita kennislóð",
"account.name.help.domain": "{domain} er netþjónninn sem hýsir upplýsingasnið um notandann og færslurnar hans.",
"account.name.help.domain_self": "{domain} er netþjónninn þinn sem hýsir upplýsingasniðið þitt og færslurnar þínar.",
@ -125,7 +125,7 @@
"account.remove_from_followers": "Fjarlægja {name} úr fylgjendum",
"account.report": "Kæra @{name}",
"account.requested_follow": "{name} hefur beðið um að fylgjast með þér",
"account.requests_to_follow_you": "Bað um að fylgjast með þér",
"account.requests_to_follow_you": "Bað um að fylgja þér",
"account.share": "Deila notandasniði fyrir @{name}",
"account.show_reblogs": "Sýna endurbirtingar frá @{name}",
"account.statuses_counter": "{count, plural, one {{counter} færsla} other {{counter} færslur}}",
@ -402,7 +402,7 @@
"collections.new_collection": "Nýtt safn",
"collections.no_collections_yet": "Engin söfn ennþá.",
"collections.remove_account": "Fjarlægja",
"collections.report_collection": "Kæra þetta safn",
"collections.report_collection": "Tilkynna þetta safn",
"collections.revoke_collection_inclusion": "Fjarlægja mig úr þessu safni",
"collections.revoke_inclusion.confirmation": "Þú varst fjarlægð/ur úr \"{collection}\"",
"collections.revoke_inclusion.error": "Upp kom villa, reyndu aftur síðar.",
@ -412,8 +412,8 @@
"collections.share_short": "Deila",
"collections.suggestions.can_not_add": "Er ekki hægt að bæta við",
"collections.suggestions.can_not_add_desc": "Þessir aðgangar gætu hafa skráð sig úr almennri birtingu eða gætu verið á netþjónum sem ekki styðja söfn.",
"collections.suggestions.must_follow": "Verður fyrst að fylgjast með",
"collections.suggestions.must_follow_desc": "Þessir aðgangar yfirfara allar beiðnir um að fylgjast m. Fylgjendur geta bætt þeim í söfn.",
"collections.suggestions.must_follow": "Verður fyrst að fylgja",
"collections.suggestions.must_follow_desc": "Þessir aðgangar yfirfara allar beiðnir um að fylgja þeim. Fylgjendur geta bætt þeim í söfn.",
"collections.topic_hint": "Bættu við myllumerki sem hjálpar öðrum að skilja aðalefni þessa safns.",
"collections.topic_special_chars_hint": "Sérstafir verða fjarlægðir við vistun",
"collections.unlisted_collections_description": "Þessi birtast ekki öðrum á notandasíðunni þinni. Þeir sem eru með tengil á þau geta séð þau.",
@ -508,9 +508,9 @@
"confirmations.discard_draft.post.title": "Henda drögum að færslunni þinni?",
"confirmations.discard_edit_media.confirm": "Henda",
"confirmations.discard_edit_media.message": "Þú ert með óvistaðar breytingar á lýsingu myndefnis eða forskoðunar, henda þeim samt?",
"confirmations.follow_to_list.confirm": "Fylgjast með og bæta á lista",
"confirmations.follow_to_list.message": "Þú þarft að fylgjast með {name} til að bæta viðkomandi á lista.",
"confirmations.follow_to_list.title": "Fylgjast með notanda?",
"confirmations.follow_to_list.confirm": "Fylgja og bæta á lista",
"confirmations.follow_to_list.message": "Þú þarft að fylgja {name} til að bæta viðkomandi á lista.",
"confirmations.follow_to_list.title": "Fylgja notanda?",
"confirmations.hide_featured_tab.confirm": "Fela flipa",
"confirmations.hide_featured_tab.intro": "Þú getur breytt þessu hvenær sem er í <i>Breyta notandasniði > Stillingar notandasniðsflipa</i>.",
"confirmations.hide_featured_tab.message": "Þetta mun fela flipann fyrir notendum á {serverName} og öðrum netþjónum sem nota nýjustu útgáfuna af Mastodon. Birting í öðrum viðmótum gæti verið á ýmsan máta.",
@ -536,7 +536,7 @@
"confirmations.redraft.message": "Ertu viss um að þú viljir eyða þessari færslu og enduvinna drögin? Eftirlæti og endurbirtingar munu glatast og svör við upprunalegu færslunni munu verða munaðarlaus.",
"confirmations.redraft.title": "Eyða og byrja ný drög að færslu?",
"confirmations.remove_from_followers.confirm": "Fjarlægja fylgjanda",
"confirmations.remove_from_followers.message": "{name} mun hætta að fylgjast með þér. Ertu viss um að þú viljir halda áfram?",
"confirmations.remove_from_followers.message": "{name} mun hætta að fylgja þér. Ertu viss um að þú viljir halda áfram?",
"confirmations.remove_from_followers.title": "Fjarlægja fylgjanda?",
"confirmations.revoke_collection_inclusion.confirm": "Fjarlægja mig",
"confirmations.revoke_collection_inclusion.message": "Þessi aðgerð er varanleg og umsjónaraðili safnsins mun ekki geta bætt þér aftur við síðar.",
@ -547,9 +547,9 @@
"confirmations.unblock.confirm": "Aflétta útilokun",
"confirmations.unblock.title": "Aflétta útilokun á {name}?",
"confirmations.unfollow.confirm": "Hætta að fylgja",
"confirmations.unfollow.title": "Hætta að fylgjast með {name}?",
"confirmations.unfollow.title": "Hætta að fylgja {name}?",
"confirmations.withdraw_request.confirm": "Taka beiðni til baka",
"confirmations.withdraw_request.title": "Taka aftur beiðni um að fylgjast með {name}?",
"confirmations.withdraw_request.title": "Taka aftur beiðni um að fylgja {name}?",
"content_warning.hide": "Fela færslu",
"content_warning.show": "Birta samt",
"content_warning.show_more": "Sýna meira",
@ -1113,7 +1113,7 @@
"report.category.title_account": "notandasnið",
"report.category.title_status": "færsla",
"report.close": "Lokið",
"report.collection_comment": "Hvers vegna viltu kæra þetta safn?",
"report.collection_comment": "Afhverju viltu tilkynna þetta safn?",
"report.comment.title": "Er eitthvað annað sem þú heldur að við ættum að vita?",
"report.forward": "Áframsenda til {target}",
"report.forward_hint": "Notandaaðgangurinn er af öðrum vefþjóni. Á einnig að senda nafnlaust afrit af kærunni þangað?",

View File

@ -410,6 +410,10 @@
"collections.search_accounts_max_reached": "Du har lagt till maximalt antal konton",
"collections.sensitive": "Känsligt",
"collections.share_short": "Dela",
"collections.suggestions.can_not_add": "Kan inte läggas till",
"collections.suggestions.can_not_add_desc": "Dessa konton kan ha valt bort upptäckt, eller kan de vara på en server som inte stöder samlingar.",
"collections.suggestions.must_follow": "Måste följa först",
"collections.suggestions.must_follow_desc": "Dessa konton granskar alla följa förfrågningar. Följare kan lägga till dem i samlingar.",
"collections.topic_hint": "Lägg till en hashtagg som hjälper andra att förstå huvudämnet i denna samling.",
"collections.topic_special_chars_hint": "Specialtecken kommer att tas bort när du sparar",
"collections.unlisted_collections_description": "Dessa visas inte på din profil till andra. Vem som helst med länken kan upptäcka dem.",

View File

@ -1122,7 +1122,7 @@ a.name-tag,
text-decoration: none;
margin-bottom: 10px;
&:hover {
&:where(a):where(:hover, :focus, :active) {
color: var(--color-text-brand);
}
}
@ -1131,14 +1131,6 @@ a.name-tag,
vertical-align: middle;
}
a.announcements-list__item__title {
&:hover,
&:focus,
&:active {
color: var(--color-text-primary);
}
}
&__action-bar {
display: flex;
justify-content: space-between;

View File

@ -53,6 +53,7 @@ class Collection < ApplicationRecord
validates :language, language: { if: :local?, allow_nil: true }
validate :tag_is_usable
validate :items_do_not_exceed_limit
validate :user_does_not_exceed_limit, on: :create
scope :with_items, -> { includes(:collection_items).merge(CollectionItem.with_accounts) }
scope :with_tag, -> { includes(:tag) }
@ -105,4 +106,11 @@ class Collection < ApplicationRecord
def items_do_not_exceed_limit
errors.add(:collection_items, :too_many, count: MAX_ITEMS) if pending_or_accepted_items.size > MAX_ITEMS
end
def user_does_not_exceed_limit
return unless local?
limit = account.user.role.collection_limit
errors.add(:base, :too_many, count: limit) if account.collections.count >= limit
end
end

View File

@ -4,15 +4,16 @@
#
# Table name: user_roles
#
# id :bigint(8) not null, primary key
# color :string default(""), not null
# highlighted :boolean default(FALSE), not null
# name :string default(""), not null
# permissions :bigint(8) default(0), not null
# position :integer default(0), not null
# require_2fa :boolean default(FALSE), not null
# created_at :datetime not null
# updated_at :datetime not null
# id :bigint(8) not null, primary key
# collection_limit :integer default(10), not null
# color :string default(""), not null
# highlighted :boolean default(FALSE), not null
# name :string default(""), not null
# permissions :bigint(8) default(0), not null
# position :integer default(0), not null
# require_2fa :boolean default(FALSE), not null
# created_at :datetime not null
# updated_at :datetime not null
#
class UserRole < ApplicationRecord
@ -104,6 +105,7 @@ class UserRole < ApplicationRecord
validates :name, presence: true, unless: :everyone?
validates :color, format: { with: CSS_COLORS }, if: :color?
validates :position, numericality: { in: (-POSITION_LIMIT..POSITION_LIMIT) }
validates :collection_limit, numericality: { only_integer: true, greater_than_or_equal_to: 0 }
validate :validate_permissions_elevation
validate :validate_position_elevation

View File

@ -3,6 +3,8 @@
class REST::RoleSerializer < ActiveModel::Serializer
attributes :id, :name, :permissions, :color, :highlighted
attribute :collection_limit, if: -> { Mastodon::Feature.collections_enabled? }
def id
object.id.to_s
end

View File

@ -32,6 +32,14 @@
%hr.spacer/
- if Mastodon::Feature.collections_enabled?
.fields-group
= form.input :collection_limit,
wrapper: :with_label
%hr.spacer/
- unless current_user.role == form.object
.field-group

View File

@ -679,7 +679,7 @@ is:
suspend_description_html: Notandaaðgangurinn og allt efni á honum mun verða óaðgengilegt og á endanum eytt út og samskipti við aðganginn verða ekki möguleg. Hægt að afturkalla innan 30 daga. Lokar öllum kærum gagnvart þessum aðgangi.
actions_description_html: Ákveddu til hvaða aðgerða eigi að taka til að leysa þessa kæru. Ef þú ákveður að refsa kærða notandaaðgangnum, verður viðkomandi send tilkynning í tölvupósti, nema ef flokkurinn <strong>Ruslpóstur</strong> sé valinn.
actions_description_remote_html: Ákveddu til hvaða aðgerða eigi að taka til að leysa þessa kæru. Þetta mun aðeins hafa áhrif á hvernig <strong>netþjónninn þinn</strong> meðhöndlar þennan fjartengda aðgang og efnið á honum.
actions_no_posts: Þessi kæra er ekki með neinar tengdar færslur til að eyða
actions_no_posts: Þessi tilkynning er ekki með neinar tengdar færslur til að eyða
add_to_report: Bæta fleiru í kæru
already_suspended_badges:
local: Þegar frystur á þessum netþjóni
@ -884,7 +884,7 @@ is:
public: Allir
landing_page:
values:
about: Um hugbúnaðinn
about: Um
local_feed: Staðbundið streymi
trends: Vinsælt
registrations:

View File

@ -160,6 +160,7 @@ en:
other: We have to make sure you're at least %{count} to use %{domain}. We won't store this.
role: The role controls which permissions the user has.
user_role:
collection_limit: Limits the number of Collections that a single user with this role can create. Please note that when you decrease this number, users who are already at this limit will not lose any Collections. But they will not be able to create additional ones.
color: Color to be used for the role throughout the UI, as RGB in hex format
highlighted: This makes the role publicly visible
name: Public name of the role, if role is set to be displayed as a badge
@ -386,6 +387,7 @@ en:
role: Role
time_zone: Time zone
user_role:
collection_limit: Maximum number of Collections per user
color: Badge color
highlighted: Display role as badge on user profiles
name: Name

View File

@ -61,6 +61,9 @@ sv:
setting_default_quote_policy_private: Inlägg som endast är för följare och som författats på Mastodon kan inte citeras av andra.
setting_default_quote_policy_unlisted: När folk citerar dig, kommer deras inlägg också att döljas från trendande tidslinjer.
setting_default_sensitive: Känslig media döljs som standard och kan visas med ett klick
setting_display_media_default: Varna innan du visar media markerad som känslig
setting_display_media_hide_all: Varna innan du visar alla medier
setting_display_media_show_all: Visa alla medier utan varning, inklusive media markerad som känslig
setting_emoji_style: Hur emojier visas. "Automatiskt" kommer att försöka använda webbläsarens emojier, men faller tillbaka till Twemoji för äldre webbläsare.
setting_quick_boosting_html: När aktiverad, klicka på %{boost_icon} Boost-ikonen för att omedelbart boosta istället för att öppna boost/citera-rullgardinsmenyn. Flyttar citering till %{options_icon} (Alternativ)-menyn.
setting_system_scrollbars_ui: Gäller endast för webbläsare som är baserade på Safari och Chrome

View File

@ -0,0 +1,7 @@
# frozen_string_literal: true
class AddCollectionLimitToUserRoles < ActiveRecord::Migration[8.1]
def change
add_column :user_roles, :collection_limit, :integer, null: false, default: 10
end
end

View File

@ -7,7 +7,7 @@ class MigrateRoles < ActiveRecord::Migration[5.2]
class User < ApplicationRecord; end
def up
load Rails.root.join('db', 'seeds', '03_roles.rb')
create_user_roles
owner_role = UserRole.find_by(name: 'Owner')
moderator_role = UserRole.find_by(name: 'Moderator')
@ -24,4 +24,30 @@ class MigrateRoles < ActiveRecord::Migration[5.2]
User.where(role_id: [admin_role.id, owner_role.id]).in_batches.update_all(admin: true) if admin_role
User.where(role_id: moderator_role.id).in_batches.update_all(moderator: true) if moderator_role
end
private
def create_user_roles
now = Time.zone.now.to_fs(:db)
safety_assured do
execute <<~SQL.squish
INSERT INTO user_roles ( id, permissions, created_at, updated_at )
VALUES ( -99, 65536, '#{now}', '#{now}' )
ON CONFLICT DO NOTHING
SQL
[
['Moderator', 10, 1308],
['Admin', 100, 983_036],
['Owner', 1000, 1],
].each do |name, position, permissions|
execute <<~SQL.squish
INSERT INTO user_roles ( name, position, highlighted, permissions, created_at, updated_at )
SELECT '#{name}', #{position}, true, #{permissions}, '#{now}', '#{now}'
WHERE NOT EXISTS ( SELECT 1 FROM user_roles WHERE name = '#{name}' )
SQL
end
end
end
end

View File

@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[8.1].define(version: 2026_04_15_133505) do
ActiveRecord::Schema[8.1].define(version: 2026_04_20_124030) do
# These are extensions that must be enabled in order to support this database
enable_extension "pg_catalog.plpgsql"
@ -1340,6 +1340,7 @@ ActiveRecord::Schema[8.1].define(version: 2026_04_15_133505) do
end
create_table "user_roles", force: :cascade do |t|
t.integer "collection_limit", default: 10, null: false
t.string "color", default: "", null: false
t.datetime "created_at", null: false
t.boolean "highlighted", default: false, null: false

View File

@ -107,7 +107,6 @@
"react-select": "^5.7.3",
"react-sparklines": "^1.7.0",
"react-textarea-autosize": "^8.4.1",
"react-toggle": "^4.1.3",
"redux-immutable": "^4.0.0",
"regenerator-runtime": "^0.14.0",
"requestidlecallback": "^0.3.0",
@ -160,7 +159,6 @@
"@types/react-router-dom": "^5.3.3",
"@types/react-sparklines": "^1.7.2",
"@types/react-test-renderer": "^18.0.0",
"@types/react-toggle": "^4.0.3",
"@types/redux-immutable": "^4.0.3",
"@types/requestidlecallback": "^0.3.5",
"@vitest/browser": "^4.1.0",

View File

@ -71,6 +71,20 @@ RSpec.describe Collection do
it { is_expected.to be_valid }
end
end
context 'when the user is already at the per-user limit of collections' do
subject { Fabricate.build(:collection, account:) }
let(:role) { Fabricate(:user_role, collection_limit: 2) }
let(:user) { Fabricate(:user, role:) }
let(:account) { user.account }
before do
Fabricate.times(2, :collection, account:)
end
it { is_expected.to_not be_valid }
end
end
describe '#item_for' do

View File

@ -31,6 +31,12 @@ RSpec.describe UserRole do
it { is_expected.to_not allow_values('x', '112233445566', '#xxyyzz').for(:color) }
end
describe 'collection_limit' do
subject { Fabricate.build :user_role }
it { is_expected.to validate_numericality_of(:collection_limit).only_integer.is_greater_than_or_equal_to(0) }
end
context 'when current_account is set' do
subject { Fabricate :user_role }

View File

@ -0,0 +1,43 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe REST::RoleSerializer do
subject { serialized_record_json(role, described_class) }
let(:everyone) do
Fabricate.build(:user_role, permissions: 0)
end
let(:role) do
Fabricate.build(:user_role, id: 2342, name: 'test role', color: '#ABC', highlighted: true, permissions: 2300, collection_limit: 11)
end
before do
allow(UserRole).to receive(:everyone).and_return(everyone)
end
it 'includes the relevant attributes' do
expect(subject)
.to include({
'id' => '2342',
'name' => 'test role',
'color' => '#ABC',
'highlighted' => true,
'permissions' => '2300',
})
end
context 'when collections are enabled', feature: :collections do
it 'includes the relevant attributes' do
expect(subject)
.to include({
'id' => '2342',
'name' => 'test role',
'color' => '#ABC',
'highlighted' => true,
'permissions' => '2300',
'collection_limit' => 11,
})
end
end
end

View File

@ -6,6 +6,7 @@ import { optimizeLodashImports } from '@optimize-lodash/rollup-plugin';
import babel from '@rolldown/plugin-babel';
import legacy from '@vitejs/plugin-legacy';
import react from '@vitejs/plugin-react';
import browserslist from 'browserslist';
import postcssPresetEnv from 'postcss-preset-env';
import Compress from 'rollup-plugin-gzip';
import { visualizer } from 'rollup-plugin-visualizer';
@ -182,6 +183,7 @@ export const config: UserConfigFnPromise = async ({ mode, command }) => {
legacy({
renderLegacyChunks: false,
modernPolyfills: true,
modernTargets: browserslist.loadConfig({ path: process.cwd() }),
}),
isProdBuild && (Compress() as PluginOption),
command === 'build' &&

View File

@ -2885,7 +2885,6 @@ __metadata:
"@types/react-router-dom": "npm:^5.3.3"
"@types/react-sparklines": "npm:^1.7.2"
"@types/react-test-renderer": "npm:^18.0.0"
"@types/react-toggle": "npm:^4.0.3"
"@types/redux-immutable": "npm:^4.0.3"
"@types/requestidlecallback": "npm:^0.3.5"
"@use-gesture/react": "npm:^10.3.1"
@ -2966,7 +2965,6 @@ __metadata:
react-sparklines: "npm:^1.7.0"
react-test-renderer: "npm:^18.2.0"
react-textarea-autosize: "npm:^8.4.1"
react-toggle: "npm:^4.1.3"
redux-immutable: "npm:^4.0.0"
regenerator-runtime: "npm:^0.14.0"
requestidlecallback: "npm:^0.3.0"
@ -4791,15 +4789,6 @@ __metadata:
languageName: node
linkType: hard
"@types/react-toggle@npm:^4.0.3":
version: 4.0.5
resolution: "@types/react-toggle@npm:4.0.5"
dependencies:
"@types/react": "npm:*"
checksum: 10c0/f557b85c96715b145bcc3beb2903f88ee3a6045ef85da0f80561c7cc2ecdc531e2d4ae121ed8ec3a1761264de25b8410653744093f37abf042201587add7ffa6
languageName: node
linkType: hard
"@types/react-transition-group@npm:^4.4.0":
version: 4.4.8
resolution: "@types/react-transition-group@npm:4.4.8"
@ -5927,11 +5916,11 @@ __metadata:
linkType: hard
"baseline-browser-mapping@npm:^2.9.0":
version: 2.9.2
resolution: "baseline-browser-mapping@npm:2.9.2"
version: 2.10.20
resolution: "baseline-browser-mapping@npm:2.10.20"
bin:
baseline-browser-mapping: dist/cli.js
checksum: 10c0/4f9be09e20261ed26f19e9b95454dcb8d8371b87983c57cd9f70b9572e9b3053577f0d8d6d91297bdb605337747680686e22f62522a6e57ae2488fcacf641188
baseline-browser-mapping: dist/cli.cjs
checksum: 10c0/3d60c9656c4c4673593aa8d0ae9aa6b69b4e018c2f585874a0e8a40cb28d0559f57ee1b2e7e44cb1e7f6aac66f658a4a3c1285901b8836d8ae31e189e30aa816
languageName: node
linkType: hard
@ -6160,9 +6149,9 @@ __metadata:
linkType: hard
"caniuse-lite@npm:^1.0.30001759, caniuse-lite@npm:^1.0.30001766":
version: 1.0.30001770
resolution: "caniuse-lite@npm:1.0.30001770"
checksum: 10c0/02d15a8b723af65318cb4d888a52bb090076898da7b0de99e8676d537f8d1d2ae4797e81518e1e30cbfe84c33b048c322e8bfafc5b23cfee8defb0d2bf271149
version: 1.0.30001790
resolution: "caniuse-lite@npm:1.0.30001790"
checksum: 10c0/eec0adc1dcb35d51e57bcfa0657493cb57ef43f0ceb03c1edcfee34d43e7a938e6beed2781118c7a5ee99d4f71d443977f08ca5a549005cf89260733af9ad3f8
languageName: node
linkType: hard
@ -6245,7 +6234,7 @@ __metadata:
languageName: node
linkType: hard
"classnames@npm:^2.2.5, classnames@npm:^2.3.2":
"classnames@npm:^2.3.2":
version: 2.5.1
resolution: "classnames@npm:2.5.1"
checksum: 10c0/afff4f77e62cea2d79c39962980bf316bacb0d7c49e13a21adaadb9221e1c6b9d3cdb829d8bb1b23c406f4e740507f37e1dcf506f7e3b7113d17c5bab787aa69
@ -12290,19 +12279,6 @@ __metadata:
languageName: node
linkType: hard
"react-toggle@npm:^4.1.3":
version: 4.1.3
resolution: "react-toggle@npm:4.1.3"
dependencies:
classnames: "npm:^2.2.5"
peerDependencies:
prop-types: ">= 15.3.0 < 19"
react: ">= 15.3.0 < 19"
react-dom: ">= 15.3.0 < 19"
checksum: 10c0/afae643981d6fb922d66830ec0e9c793e729d7a55e3974edf212965acc8dc51f0d098ae6e409461d3ff2f87fe027dc3556a1a746f8472ed3a79c8878bf338fee
languageName: node
linkType: hard
"react-transition-group@npm:^4.3.0":
version: 4.4.5
resolution: "react-transition-group@npm:4.4.5"