diff --git a/app/assets/javascripts/community_order.js b/app/assets/javascripts/community_order.js new file mode 100644 index 000000000..e7c00df8b --- /dev/null +++ b/app/assets/javascripts/community_order.js @@ -0,0 +1,34 @@ +document.addEventListener('DOMContentLoaded', () => { + const pref = /** @type {HTMLElement} */(document.querySelector(".community-sortable")); + const el = /** @type {HTMLElement} */(pref.querySelector(".sortable")); + const input = /** @type {HTMLInputElement} */(pref.querySelector(".js-user-pref")); + + const sortable = Sortable.create(el, { + store: { + get: () => input.value.split(','), + set: self => { + input.value = self.toArray().join(','); + input.dispatchEvent(new Event("change")); + } + } + }); + + document.querySelectorAll('.community-sortable-list-item').forEach(el => { + el.querySelector('.sort-top-btn')?.addEventListener('click', _ => { + el.parentElement.insertAdjacentElement('afterbegin', el); + sortable.save(); + }); + el.querySelector('.sort-up-btn')?.addEventListener('click', _ => { + el.previousElementSibling?.insertAdjacentElement('beforebegin', el); + sortable.save(); + }); + el.querySelector('.sort-down-btn')?.addEventListener('click', _ => { + el.nextElementSibling?.insertAdjacentElement('afterend', el); + sortable.save(); + }); + el.querySelector('.sort-bottom-btn')?.addEventListener('click', _ => { + el.parentElement.insertAdjacentElement('beforeend', el); + sortable.save(); + }); + }) +}); \ No newline at end of file diff --git a/app/assets/stylesheets/users.scss b/app/assets/stylesheets/users.scss index 3835a2878..05118f9fe 100644 --- a/app/assets/stylesheets/users.scss +++ b/app/assets/stylesheets/users.scss @@ -187,6 +187,38 @@ img { } } +.user-pref.user-block-pref { + flex-direction: column; + align-items: flex-start; +} + +.community-sortable { + .community-sortable-list-item { + display: flex; + justify-content: flex-start; + align-items: center; + cursor: grab; + img { + max-height: 1.5rem; + } + .img { + width: 100px; + padding: 10px; + } + .label { + flex-grow: 1; + } + .sortable-buttons { + display: flex; + flex-direction: column; + } + .community-label { + display: flex; + align-items: center; + } + } +} + $sizes: (16, 32, 40, 48, 64, 128, 256); @each $size in $sizes { diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 62372c684..cb94a1de0 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -32,7 +32,7 @@ def upload end def dashboard - @communities = Community.all + @communities = Community.all.user_preferred_order(current_user) @edits = Post.unscoped do SuggestedEdit.unscoped.joins(:post).where(active: true).group(Arel.sql('posts.category_id')).count end diff --git a/app/models/community.rb b/app/models/community.rb index d6eca7ac3..901bf3eb1 100644 --- a/app/models/community.rb +++ b/app/models/community.rb @@ -5,4 +5,23 @@ class Community < ApplicationRecord default_scope { where(is_fake: false) } validates :host, uniqueness: { case_sensitive: false } + + scope :user_preferred_order, lambda { |user| + if user.nil? + all + else + user_order = user.preference('community_order', community: false) + .split(',') + .map(&:to_i) + + # For some reason, even though we pass in filter: false, + # if user_order is empty, in_order_of still returns an empty array. + # Therefore we have to special case this + if user_order.empty? + all + else + in_order_of(:id, user_order, filter: false) + end + end + } end diff --git a/app/views/application/dashboard.html.erb b/app/views/application/dashboard.html.erb index 2843a3336..cdea593ce 100644 --- a/app/views/application/dashboard.html.erb +++ b/app/views/application/dashboard.html.erb @@ -8,6 +8,10 @@

All communities, with their categories. You can also see the communities from the dropdown arrow at the top right of each page. + <% if user_signed_in? %> + You may change the order in which these communities appear in + <%= link_to 'your preferences', user_preferences_path %>. + <% end %>

<% @communities.each do |c| %> diff --git a/app/views/layouts/_head.html.erb b/app/views/layouts/_head.html.erb index 700655931..9d834dd3e 100644 --- a/app/views/layouts/_head.html.erb +++ b/app/views/layouts/_head.html.erb @@ -34,6 +34,7 @@ <%= stylesheet_link_tag 'application', media: 'all' %> <%= javascript_include_tag "https://cdn.jsdelivr.net/npm/jquery@2.2.2/dist/jquery.min.js" %> +<%= javascript_include_tag "https://cdn.jsdelivr.net/npm/sortablejs@latest/Sortable.min.js" %> <%= javascript_include_tag "https://cdn.jsdelivr.net/npm/moment@2.13.0/min/moment.min.js" %> <%= javascript_include_tag "https://cdn.jsdelivr.net/npm/select2@4.0.12/dist/js/select2.min.js" %> <% if SiteSetting['DonationsEnabled'] && (SiteSetting['LoadStripeEverywhere'] || controller_name == 'donations') %> diff --git a/app/views/layouts/_header.html.erb b/app/views/layouts/_header.html.erb index d228bede2..82c84bfe6 100644 --- a/app/views/layouts/_header.html.erb +++ b/app/views/layouts/_header.html.erb @@ -133,9 +133,12 @@

Communities

+ <% if user_signed_in? %> + <%= link_to 'Edit order', user_preferences_path, class: 'button is-muted is-small' %> + <% end %>
<% logo_paths = SiteSetting.all_communities('SiteLogoPath') %> - <% Community.where(hidden: false).each do |c| %> + <% Community.where(hidden: false).user_preferred_order(current_user).each do |c| %>
<% logo_setting = logo_paths[c.id] %> <%= link_to "//#{c.host}" do %> diff --git a/app/views/users/_prefs.html.erb b/app/views/users/_prefs.html.erb index 7239b2f9a..ae5520e97 100644 --- a/app/views/users/_prefs.html.erb +++ b/app/views/users/_prefs.html.erb @@ -1,27 +1,68 @@ <% prefs.each do |name, value| %> <% pref_config = AppConfig.preferences[name] %> - + <% case pref_config['type'] %> + <% when 'boolean' %> + <%= render 'prefs_inline', name: name, pref_config: pref_config do %> + + data-community="<%= community %>" /> + <% end %> + <% when 'choice' %> + <%= render 'prefs_inline', name: name, pref_config: pref_config do %> + <%= select_tag nil, + options_for_select(preference_choice(pref_config), selected: value), + class: 'js-user-pref form-element', + data: { pref: name, community: community } %> + <% end %> + <% when 'integer' %> + <%= render 'prefs_inline', name: name, pref_config: pref_config do %> + + <% end %> + <% when 'string' %> + <%= render 'prefs_inline', name: name, pref_config: pref_config do %> + + <% end %> + <% when 'community_order' %> + <%= render 'prefs_block', name: name, pref_config: pref_config do %> +
+
+ Reorder + <% logo_paths = SiteSetting.all_communities('SiteLogoPath') %> +
    + <% Community.where(hidden: false).user_preferred_order(current_user).each do |c| %> +
  1. + +
    + + +
    +
    + + +
    +
    + <% logo_setting = logo_paths[c.id] %> + <% if logo_setting.present? %> +
    + <%= c.name %> +
    + <% end %> +
    + <%= c.name %> +
    +
    +
  2. + <% end %> +
+ +
+
+ <% end %> + <% end %> <% end %> diff --git a/app/views/users/_prefs_block.erb b/app/views/users/_prefs_block.erb new file mode 100644 index 000000000..7cec49cb0 --- /dev/null +++ b/app/views/users/_prefs_block.erb @@ -0,0 +1,9 @@ +
+
+ <%= name.humanize %>
+ <%= pref_config['description'] %> +
+
+ <%= yield %> +
+
\ No newline at end of file diff --git a/app/views/users/_prefs_inline.erb b/app/views/users/_prefs_inline.erb new file mode 100644 index 000000000..838f296ca --- /dev/null +++ b/app/views/users/_prefs_inline.erb @@ -0,0 +1,9 @@ + \ No newline at end of file diff --git a/config/config/preferences.yml b/config/config/preferences.yml index 240b901ba..8aa7b4e41 100644 --- a/config/config/preferences.yml +++ b/config/config/preferences.yml @@ -2,7 +2,7 @@ # Schema: # # preference_name: -# type: boolean | string | integer | choice +# type: boolean | string | integer | choice | community_order # description: Free-form description # choice: # - only @@ -100,3 +100,10 @@ collapse_related_posts: Collapse the Related Posts widget in the sidebar by default. default: 'false' global: true + +community_order: + type: community_order + description: > + Order of communities in the dashboard and dropdown + default: '' + global: true \ No newline at end of file diff --git a/global.d.ts b/global.d.ts index 5ecaaa1d6..ce5f3f5f3 100644 --- a/global.d.ts +++ b/global.d.ts @@ -1,3 +1,5 @@ +declare var Sortable: typeof import("sortablejs"); + interface ElementOffset { top: number; left: number; diff --git a/package-lock.json b/package-lock.json index 33a5deaed..6a7507084 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "@types/jquery": "^3.5.32", "@types/prettier": "2.7.3", "@types/select2": "^4.0.63", + "@types/sortablejs": "1.15.9", "dompurify": "3.4.13", "moment": "2.13.0", "prettier-plugin-brace-style": "0.8.1", @@ -48,6 +49,13 @@ "integrity": "sha512-xzLEyKB50yqCUPUJkIsrVvoWNfFUbIZI+RspLWt8u+tIW/BetMBZtgV2LY/2o+tYH8dRvQ+eoPf3NdhQCcLE2w==", "dev": true }, + "node_modules/@types/sortablejs": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/@types/sortablejs/-/sortablejs-1.15.9.tgz", + "integrity": "sha512-7HP+rZGE2p886PKV9c9OJzLBI6BBJu1O7lJGYnPyG3fS4/duUCcngkNCjsLwIMV+WMqANe3tt4irrXHSIe68OQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/trusted-types": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", @@ -172,6 +180,12 @@ "integrity": "sha512-xzLEyKB50yqCUPUJkIsrVvoWNfFUbIZI+RspLWt8u+tIW/BetMBZtgV2LY/2o+tYH8dRvQ+eoPf3NdhQCcLE2w==", "dev": true }, + "@types/sortablejs": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/@types/sortablejs/-/sortablejs-1.15.9.tgz", + "integrity": "sha512-7HP+rZGE2p886PKV9c9OJzLBI6BBJu1O7lJGYnPyG3fS4/duUCcngkNCjsLwIMV+WMqANe3tt4irrXHSIe68OQ==", + "dev": true + }, "@types/trusted-types": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", diff --git a/package.json b/package.json index d61965491..394f7c27d 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "@types/jquery": "^3.5.32", "@types/prettier": "2.7.3", "@types/select2": "^4.0.63", + "@types/sortablejs": "1.15.9", "dompurify": "3.4.13", "moment": "2.13.0", "prettier-plugin-brace-style": "0.8.1",