Compare commits

..

180 Commits

Author SHA1 Message Date
Jarek Radosz
38fdd842f5
UX: Fix chat separator alignment (#20669)
Also: work around 1px svg shift in scroll-to-bottom button
2023-03-18 18:03:54 +01:00
Joffrey JAFFEUX
aeab38aff1
UX: disable arrow up to edit if last message is not editable (#20729) 2023-03-17 23:08:10 +01:00
Joffrey JAFFEUX
aa8eff5e16
FIX: ensures updateLastRead is called when receiving a message (#20728)
This behavior is hard to test as it's mostly fixing a race condition: User A sends a message at the same time than User B, which as a result doesn't cause a scroll for the second message and we don't update last read unless we do a small up and down scroll.

`updateLastRead` is debounced so it has no direct consequences to call it slightly more often than what should ideally be needed.
2023-03-17 22:46:59 +01:00
Daniel Waterworth
293cb7bde2
FIX: An ember build is required to run the system tests (#20725) 2023-03-17 13:20:49 -05:00
Joffrey JAFFEUX
cfee0cfee9
FIX: ensures lightbox is working after collapse/expand (#20724)
Prior to this fix, the upload was removed from DOM when collapsed and not decorated again on expand, which was causing lightbox to not get reapplied. The fix is reverting to previous state where content was not removed from DOM.
2023-03-17 18:26:32 +01:00
Joffrey JAFFEUX
c5e5b6d5ab
DEV: fixes a flakey spec (#20721) 2023-03-17 18:01:19 +01:00
Joffrey JAFFEUX
184ce647ea
FIX: correctly infer polymorphic class from bookmarkable type (#20719)
Prior to this change `registered_bookmarkable` would return `nil` as  `type` in `Bookmark.registered_bookmarkable_from_type(type)` would be `ChatMessage` and we registered a `Chat::Message` class.

This commit will now properly rely on each model `polymorphic_class_for(name)` to help us infer the proper type from a a `bookmarkable_type`.

Tests have also been added to ensure that creating/destroying chat message bookmarks is working correctly.

---

Longer explanation

Currently when you save a bookmark in the database, it's associated to another object through a polymorphic relationship, which will is represented by two columns: `bookmarkable_id` and `bookmarkable_type`. The `bookmarkable_id` contains the id of the relationship (a post ID for example) and the `bookmarkable_type` contains the type of the object as a string by default, (`"Post"` for example).

Chat plugin just started namespacing objects, as a result a model named `ChatMessage` is now named `Chat::Message`, to avoid complex and risky migrations we rely on methods provided by rails to alter the `bookmarkable_type` when we save it: we want to still save it as `"ChatMessage"` and not `"Chat::Message"`. And, to retrieve the correct model when we load the bookmark from the database: we want `"ChatMessage"` to load the `Chat::Message` model and not the `ChatMessage`model which doesn't exist anymore.

On top of this the bookmark codepath is allowing plugins to register types and will check against these types, so we alter this code path to be able to do a similar ChatMessage <-> Chat::Message dance and allow to check the type is valid. In the specific case of this commit, we were retrieving a `"ChatMessage"` bookmarkable_type from the DB and looking for it in the registered bookmarkable types which contain `Chat::Message` and not `ChatMessage`.
2023-03-17 17:20:24 +01:00
TheJammiestDodger
f57ba758ce
UX: Update Install Popular items and links (#20688)
* UX: Update 'Install Popular' items and links

* Update popular-themes.js

* Update popular-themes.js

* Update popular-themes.js

* Lint

---------

Co-authored-by: Penar Musaraj <pmusaraj@gmail.com>
2023-03-17 16:05:36 +00:00
Joffrey JAFFEUX
12a18d4d55
DEV: properly namespace chat (#20690)
This commit main goal was to comply with Zeitwerk and properly rely on autoloading. To achieve this, most resources have been namespaced under the `Chat` module.

- Given all models are now namespaced with `Chat::` and would change the stored types in DB when using polymorphism or STI (single table inheritance), this commit uses various Rails methods to ensure proper class is loaded and the stored name in DB is unchanged, eg: `Chat::Message` model will be stored as `"ChatMessage"`, and `"ChatMessage"` will correctly load `Chat::Message` model.
- Jobs are now using constants only, eg: `Jobs::Chat::Foo` and should only be enqueued this way

Notes:
- This commit also used this opportunity to limit the number of registered css files in plugin.rb
- `discourse_dev` support has been removed within this commit and will be reintroduced later

<!-- NOTE: All pull requests should have tests (rspec in Ruby, qunit in JavaScript). If your code does not include test coverage, please include an explanation of why it was omitted. -->
2023-03-17 14:24:38 +01:00
David Taylor
74349e17c9
DEV: Migrate remaining admin classes to native syntax (#20717)
This commit was generated using the ember-native-class-codemod along with a handful of manual updates
2023-03-17 12:25:05 +00:00
David Taylor
1161c980f2
DEV: Resolve and unsilence ember.built-in-components deprecation (#20716)
- Install `@ember/legacy-built-in-components` and update our import statements to use it
- Remove our custom attributeBinding extensions of `TextField` and `TextArea`. Modern ember 'angle bracket syntax' allows us to apply html attributes to a component's element without needing attributeBindings
2023-03-17 11:55:29 +00:00
David Taylor
5e5024d3e7
DEV: Resolve and unsilence ember-global deprecation (#20702)
One of the problems here was coming from the ember-jquery addon. This commit skips the problematic shim from the addon and re-implements in Discourse. This hack will only be required short-term - we'll be totally dropping the ember-jquery integration as part of our upgrade to Ember 4.x.

Removing this shim means we can also remove our `discourse-ensure-deprecation-order` dummy addon which was ensuring that the ember-jquery-triggered deprecation was covered by ember-cli-deprecation-workflow.
2023-03-17 11:22:12 +00:00
David Taylor
64557c4076
DEV: Update admin models to native class syntax (#20704)
This commit was generated using the ember-native-class-codemod along with a handful of manual updates
2023-03-17 10:18:42 +00:00
David Taylor
303f97ce89
PERF: Use native postgres upsert for ApplicationRequest (#20706)
Using `create_or_find_by!`, followed by `update_all!` requires two or three queries (two when the row doesn't already exist, three when it does). Instead, we can use postgres's native `INSERT ... ON CONFLICT ... DO UPDATE SET` feature to do the logic in a single atomic call.
2023-03-17 09:35:29 +00:00
Blake Erickson
6b5743ba3c
Version bump to v3.1.0.beta3 (#20712) 2023-03-16 17:51:54 -06:00
Penar Musaraj
32ad46c551
UX: Adjust menu panels on iOS (#20703) 2023-03-16 19:23:15 -04:00
Blake Erickson
5103d249aa DEV: Skip chat channel test
The screenshot looks good, but the test appears to be bad. Skipping this
test for now, will follow-up later.
2023-03-16 15:27:09 -06:00
Ted Johansson
39c2f63b35 SECURITY: Add FinalDestination::FastImage that's SSRF safe 2023-03-16 15:27:09 -06:00
Blake Erickson
6dcb099547 FIX: Escaped mentions in chat excerpts
Mentions are now displayed as using the non-cooked message which fixes
the problem. This is not ideal. I think we might want to rework how
these excerpts are created and rendered in the near future.

Co-authored-by: Jan Cernik <jancernik12@gmail.com>
2023-03-16 15:27:09 -06:00
Blake Erickson
a373bf2a01 SECURITY: XSS on chat excerpts
Non-markdown tags weren't being escaped in chat excerpts. This could be
triggered by editing a chat message containing a tag (self XSS), or by
replying to a chat message with a tag (XSS).

Co-authored-by: Jan Cernik <jancernik12@gmail.com>
2023-03-16 15:27:09 -06:00
Alan Guo Xiang Tan
fd16eade7f SECURITY: SSRF protection bypass with IPv4-mapped IPv6 addresses
As part of this commit, we've also expanded our list of private IP
ranges based on
https://www.iana.org/assignments/iana-ipv4-special-registry/iana-ipv4-special-registry.xhtml
and https://www.iana.org/assignments/iana-ipv6-special-registry/iana-ipv6-special-registry.xhtml
2023-03-16 15:27:09 -06:00
Alan Guo Xiang Tan
52ef44f43b SECURITY: Monkey-patch web-push gem to use safer HTTP client
`FinalDestination::HTTP` is our patch of `Net::HTTP` which defend us
against SSRF and DNS rebinding attacks.
2023-03-16 15:27:09 -06:00
Blake Erickson
d89b537d8f SECURITY: Fix XSS in full name composer reply
We are using htmlSafe when rendering the name field so we need to escape
any html being passed in.
2023-03-16 15:27:09 -06:00
Andrei Prigorshnev
7dd317b875
DEV: add test cases for email notifications about channel-wide mentions (#20691)
A follow-up to e6c04e2d.
2023-03-16 21:43:56 +04:00
Penar Musaraj
c213cc7211
DEV: Fix tag route fixture param (#20693)
This tag route is /tag/important/l/latest.json so the tag name should also be important.
2023-03-16 11:27:04 -04:00
Loïc Guitaut
0bd64788d2 SECURITY: Rate limit the creation of backups 2023-03-16 16:09:22 +01:00
TheJammiestDodger
272c31023d
UX: Change JPEG to JPG for search consistency (#20698) 2023-03-16 14:53:29 +00:00
David Taylor
150a6601c0
DEV: Check Zeitwerk eager loading in GitHub CI (#20699)
In production, `eager_load=true`. This sometimes leads to boot errors which are not present in dev/test environments. Running `zeitwerk:check` in CI will help us to pick up on any errors early.

This commit also introduces a `DISCOURSE_ZEITWERK_EAGER_LOAD` environment variable to make it easier to toggle the behaviour when developing locally.
2023-03-16 14:22:16 +00:00
David Taylor
9d1423b5aa
DEV: Drop impossible conditional from admin-logs-staff-action-logs (#20687)
`Object.keys(filters)` will never return 0
2023-03-16 12:27:27 +00:00
David Taylor
5d46a16ca5
DEV: Cleanup unrelated comment from development.rb (#20697)
This comment has nothing to do with the `eager_load` configuration. It must be left over from some historical refactoring. Removing to avoid confusion.
2023-03-16 11:23:34 +00:00
Daniel Waterworth
164b60cd07 DEV: Optionally, run system tests in docker:test 2023-03-15 16:46:48 -05:00
Daniel Waterworth
5324216740 DEV: Use rspec instead of turbo_rspec with one core 2023-03-15 16:46:48 -05:00
Andrei Prigorshnev
94fd884297
DEV: remove unused string (#20662)
The button was removed in fca6805a
2023-03-16 00:15:07 +04:00
David Taylor
c190994046
DEV: Update admin modal controllers to native class syntax (#20685)
This commit was generated using the ember-native-class-codemod along with a handful of manual updates
2023-03-15 17:39:33 +00:00
dependabot[bot]
62bbdd25ab
Build(deps): Bump @babel/core in /app/assets/javascripts (#20681)
Bumps [@babel/core](https://github.com/babel/babel/tree/HEAD/packages/babel-core) from 7.21.0 to 7.21.3.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.21.3/packages/babel-core)

---
updated-dependencies:
- dependency-name: "@babel/core"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-15 14:29:16 +01:00
dependabot[bot]
2ea6675671
Build(deps): Bump sass from 1.59.2 to 1.59.3 in /app/assets/javascripts (#20682)
Bumps [sass](https://github.com/sass/dart-sass) from 1.59.2 to 1.59.3.
- [Release notes](https://github.com/sass/dart-sass/releases)
- [Changelog](https://github.com/sass/dart-sass/blob/main/CHANGELOG.md)
- [Commits](https://github.com/sass/dart-sass/compare/1.59.2...1.59.3)

---
updated-dependencies:
- dependency-name: sass
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-15 14:27:17 +01:00
dependabot[bot]
e50f51fa85
Build(deps): Bump rack-test from 2.0.2 to 2.1.0 (#20679)
Bumps [rack-test](https://github.com/rack/rack-test) from 2.0.2 to 2.1.0.
- [Release notes](https://github.com/rack/rack-test/releases)
- [Changelog](https://github.com/rack/rack-test/blob/main/History.md)
- [Commits](https://github.com/rack/rack-test/compare/v2.0.2...v2.1.0)

---
updated-dependencies:
- dependency-name: rack-test
  dependency-type: indirect
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-15 14:26:43 +01:00
David Taylor
e700f0af93
DEV: Update admin routes to native class syntax (#20686) 2023-03-15 13:17:51 +00:00
David Taylor
7288bc277b
DEV: Update docker_test to checkout specific branch by default (#20684)
Previously, FETCH_HEAD would always point to tests-passed because our base docker image was configured to only fetch the tests-passed branch. Since https://github.com/discourse/discourse_docker/commit/53bbacc882, we switched to a partial clone which means that `git fetch; git checkout FETCH_HEAD` will checkout whichever remote branch is the first alphabetically. This commit makes the checkout more specific to avoid this issue.
2023-03-15 10:54:47 +00:00
David Taylor
be354e7950
DEV: Update admin controllers to native class syntax (#20674)
This commit was generated using the ember-native-class-codemod along with a handful of manual updates
2023-03-15 09:42:12 +00:00
Daniel Waterworth
84f590ab83
DEV: Store theme sprites in the DB (#20501)
Let's avoid fetching sprites from the CDN during page rendering.
2023-03-14 13:11:45 -05:00
Andrei Prigorshnev
e6c04e2dc2
FIX: do not send emails when channel-wide mentions are disabled in a channel (#20677)
This regressed with the commit fa543cd. Starting from that commit, we create mention records even if a user shouldn't be notified. So when sending emails, we should be making sure if a notification was actually created for a mention. This is essentially the whole fix that we need here. Tests will be provided in a following PR.
2023-03-14 21:45:05 +04:00
Blake Erickson
d0c6b33cc2
SECURITY: Bump Rails to v7.0.4.3 (#20675) 2023-03-14 10:19:31 -06:00
Isaac Janzen
dfd6d6b999
FIX: Latest post created_at on topic-timeline not updating (#20665)
# Context
https://meta.discourse.org/t/timeline-timestamp-not-updating/256447/1

During the upgrade of the topic-timeline to glimmer the "latest post" timestamp was not updating on the timeline in relation to the relative age of the post. It was only updating on a hard refresh.

# Fix
Use the `age-with-tooltip` helper to update the created_at date automatically as time passes.

# Additional
Add the ability to pass params to `age-with-tooltip` so that we can include options like `addAgo` and `defaultFormat`
2023-03-14 11:08:23 -05:00
Kris
22a7818399
FIX: update LoadMore selector for user tables (#20676) 2023-03-14 11:10:51 -04:00
Penar Musaraj
a208ed0925
UX: Improve menu panel height fallback for older browsers (#20673)
Tested with Chrome <100, Firefox <100 and Samsung Internet 20, using
percentage-based height here works better for these browsers.

This was especially a problem for the Samsung Internet browser, because
it previously was hiding the "Dismiss" button on the user profile menu.
2023-03-14 10:41:38 -04:00
Discourse Translator Bot
4cf065c480
Update translations (#20671) 2023-03-14 15:04:54 +01:00
David Taylor
d12f1878c9
UX: Improve safe-mode copy (#20670)
Safe-mode only applies to client-side customizations - let's make that clearer
2023-03-14 13:00:52 +00:00
David Taylor
b5721b7b4f
FIX: default_list_filter = none navigation and preloading (#20641)
When a category has default_list_filter=none, there were a number of issues which this commit resolves:

1. When using the breadcrumbs to navigate a `default_list_filter=none` category, adding a tag filter would not apply the no-subcategories filter, but the subcategories dropdown would still say 'none'. This commit adjusts `getCategoryAndTagUrl` so that `/none` is added to the URL

2. When landing on `/tags/c/{slug}/{id}/{tag}`, for a default_list_filter=none category, it would include subcategories. This commit introduces a client-side redirect to match the behavior of `/c/{slug}/{id}`

3. When directly navigating to `/c/{slug}/{id}`, it was correctly redirecting to `/c/{slug}/{id}/none`, BUT it was still using the preloaded data for the old route. This has been happening since e7a84948. Prior to that, the preloaded data was discarded and a new JSON request was made to the server. This commit restores that discarding behavior. In future we may want to look into making this more efficient.

System specs are introduced to provide end-end testing of this functionality
2023-03-14 10:46:05 +00:00
Jarek Radosz
bb317bd554
DEV: Update the rubocop setup (#20668) 2023-03-14 11:42:11 +01:00
dependabot[bot]
f75afc0979
Build(deps): Bump rack from 2.2.6.3 to 2.2.6.4 (#20667)
Bumps [rack](https://github.com/rack/rack) from 2.2.6.3 to 2.2.6.4.
- [Release notes](https://github.com/rack/rack/releases)
- [Changelog](https://github.com/rack/rack/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rack/rack/compare/v2.2.6.3...v2.2.6.4)

---
updated-dependencies:
- dependency-name: rack
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-14 09:40:46 +01:00
Loïc Guitaut
8a8f2758d5 DEV: Refactor Jobs::UserEmail a little 2023-03-14 09:23:06 +01:00
David Taylor
964f37476d
FIX: TopicQuery for NULL category.topic_id (#20664)
Our schema allows `category.topic_id` to be NULL. Null values shouldn't actually happen in production, but it is very common in tests because `Fabricate(:category)` skips creating the definition topic to improve performance. Before this commit, a NULL category.topic_id would cause all subcategory topics to be excluded from a TopicQuery result. This is because, in postgres, `NULL <> anything` is falsy. Instead, we can use `IS DISTINCT FROM`, which will return true when NULL is compared to a non-NULL value.
2023-03-13 19:33:26 +00:00
Rafael dos Santos Silva
0a5b078ac7
FEATURE: Hook for suggested topic customization (#20618) 2023-03-13 15:37:49 -03:00
Isaac Janzen
ab442058c0
FIX: Broken topic-timeline summarize topic button (#20661)
# Context
During the octane upgrade of the Topic Timeline the `summarize-topic` button was neglected, leaving it in a broken state. 

# Fix
Update the button to replicate the original functionality


<img width="351" alt="Screenshot 2023-03-13 at 12 41 25 PM" src="https://user-images.githubusercontent.com/50783505/224785657-fc8124fe-f1d9-4cc8-917b-9cd859517da3.png">

_updated timeline with summarize button_
2023-03-13 13:12:33 -05:00
Andrei Prigorshnev
72726830b5
FIX: anonymous users cannot load topics with mentions with a user status that has an end date (#20660)
Steps to reproduce:
1. Create a post with a mention of a user that has user status with an end date
2. Try to load the topic with that post as an anonymous user

You'll see a topic with blank content.
2023-03-13 19:57:09 +04:00
David Taylor
f91af69ec4
FIX: Avatar upload error (#20658)
Followup to 4f9afabf87
2023-03-13 10:59:42 +00:00
dependabot[bot]
59e5485408
Build(deps): Bump sass from 1.58.3 to 1.59.2 in /app/assets/javascripts (#20656)
Bumps [sass](https://github.com/sass/dart-sass) from 1.58.3 to 1.59.2.
- [Release notes](https://github.com/sass/dart-sass/releases)
- [Changelog](https://github.com/sass/dart-sass/blob/main/CHANGELOG.md)
- [Commits](https://github.com/sass/dart-sass/compare/1.58.3...1.59.2)

---
updated-dependencies:
- dependency-name: sass
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-13 11:01:41 +01:00
dependabot[bot]
05e713d098
Build(deps): Bump node-fetch from 3.3.0 to 3.3.1 in /app/assets/javascripts (#20655)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-13 08:45:43 +08:00
dependabot[bot]
b5bb41493a
Build(deps): Bump eslint from 8.35.0 to 8.36.0 in /app/assets/javascripts (#20652)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-13 08:45:24 +08:00
dependabot[bot]
8c653cbc91
Build(deps): Bump sinon from 15.0.1 to 15.0.2 in /app/assets/javascripts (#20653)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-13 08:12:35 +08:00
dependabot[bot]
ad6edfc08a
Build(deps): Bump jsdom from 21.1.0 to 21.1.1 in /app/assets/javascripts (#20651)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-13 08:12:19 +08:00
dependabot[bot]
06848eaf49
Build(deps): Bump sass-embedded from 1.58.3 to 1.59.2 (#20649)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-13 08:11:06 +08:00
dependabot[bot]
e9a04dd6f1
Build(deps): Bump rspec-mocks from 3.12.3 to 3.12.4 (#20648)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-13 08:10:48 +08:00
dependabot[bot]
8793297792
Build(deps): Bump logster from 2.11.4 to 2.12.2 (#20647)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-13 08:10:31 +08:00
dependabot[bot]
173a3b4d02
Build(deps): Bump webpack from 5.76.0 to 5.76.1 in /app/assets/javascripts (#20654)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-13 08:10:09 +08:00
Alan Guo Xiang Tan
e4b11e7643
FEATURE: Only list watching group messages in messages notifications panel (#20630)
Why is this change required?

Prior to this change, we would list all group messages that a user
has access to in the user menu messages notifications panel dropdown.
However, this did not respect the topic's notification level setting and
group messages which the user has set to 'normal' notification level were
being displayed

What does this commit do?

With this commit, we no longer display all group messages that a user
has access to. Instead, we only display group messages that a user is
watching in the user menu messages notifications panel dropdown.

Internal Ref: /t/94392
2023-03-13 08:09:38 +08:00
Kris
44bc284e0f
UX: avoid overflow clipping descenders (#20643) 2023-03-10 16:49:06 -05:00
Kris
03e3fd742e
UX: some admin theme list style adjustments (#20625) 2023-03-10 15:15:31 -05:00
Blake Erickson
943068a634
FIX: Welcome topic banner showing after general category is deleted (#20639)
If you happen to delete the general category before editing the welcome
topic, the banner will still display. This fix adds a after destroy hook
that will clear the entries for the welcome topic banner in the redis
cache.
2023-03-10 12:33:12 -07:00
Joshua Rosenfeld
5172749638
UX: improves site setting description for discourse_connect_url (#20642)
Site Setting does not expect a trailing slash, and will throw an error when attempting to save such a URL.
2023-03-10 13:44:56 -05:00
Joffrey JAFFEUX
727100d1e2
DEV: adds a addChatDrawerStateCallback API (#20640)
Usage:

```javascript
api.addChatDrawerStateCallback(({ isDrawerActive, isDrawerExpanded }) => {
  // do something
});
```

Note this commit also uses this new API to add a css class (chat-drawer-active) on the body when the drawer is active.
2023-03-10 18:49:59 +01:00
David Taylor
6bb89ffea8
DEV: Resolve d-button-action-string deprecation in exception.hbs (#20638) 2023-03-10 16:56:40 +00:00
Joffrey JAFFEUX
ada89d6124
FIX: ensures edited message is correctly re-decorated (#20637) 2023-03-10 17:06:13 +01:00
Joffrey JAFFEUX
60ce3d24fa
FIX: more consistent scroll to bottom (#20634)
This fix uses direct `scrollTop` manipulation instead of `scrollIntoView` when we are certain we actually want the bottom of the screen. This avoids a range of issues especially in safari but also chrome where the scroll position was not correct at the end of `scrollIntoView`, especially due to images.
2023-03-10 16:25:21 +01:00
Andrei Prigorshnev
7df40fc905
DEV: do not fabricate a notification when fabricating a chat_mention (#20636)
This is just a little clean-up in tests. In the past, when creating a `chat_mention` 
record, we always created a related notification. Starting from fa543cda 
notifications and chat_mentions are fully decoupled from each other. So if we're 
testing just chat mentions there is no need to fabricate notifications for them.
2023-03-10 18:32:33 +04:00
David Taylor
3e8d349465
DEV: Correct core test run detection for theme-qunit (#20635)
Followup to 8f1a5c9392
2023-03-10 12:40:59 +00:00
David Taylor
8f1a5c9392
DEV: Fail core JS test runs if deprecations are triggered (#20614)
It's important to keep our core log output as clean as possible to avoid 'crying wolf', and so that any deprecations triggered by plugin/theme tests are indeed caused by that theme/plugin, and not core.

This commit will make the core test suite fail if any deprecations are triggered. If a new deprecation is introduced (e.g. as part of a dependency update) and we need more time to resolve it it can be silenced via ember-deprecation-workflow.

This does not affect plugin/theme test runs.
2023-03-10 10:39:42 +00:00
David Taylor
270e98e45f
DEV: Include ember deprecation messages in production builds (#20587)
By default, Ember uses a babel transformation to strip out calls to `deprecate()` in production builds. Given that Discourse is a development platform for third-party themes/plugins, having deprecation messages visible in production is essential - many themes/plugins do not have comprehensive test-suites, and rely on production feedback to prompt changes. This commit patches Ember to print its deprecation messages to the console in production. In future we intend to improve the visibility of these to hosting providers and/or site admins.

There are two main parts to this commit:

1. Use yarn's 'resolutions' feature to point `babel-plugin-debug-macros` to a discourse-owned fork. This fork prevents `deprecate()` calls from being stripped. Relevant change can be found at https://github.com/discourse/babel-plugin-debug-macros/commit/d179d613bf

2. Introduce a production shim for Ember's deprecation library, including the `registerDeprecationHandler` API. The default implementation is stripped out of production builds via an `if(DEBUG)` wrapper.

Long term we hope that this kind of functionality can be made available in Ember itself via a flag.
2023-03-10 10:37:28 +00:00
dependabot[bot]
cd3c35418c
Build(deps): Bump prettier_print from 1.2.0 to 1.2.1 (#20620)
Bumps [prettier_print](https://github.com/ruby-syntax-tree/prettier_print) from 1.2.0 to 1.2.1.
- [Release notes](https://github.com/ruby-syntax-tree/prettier_print/releases)
- [Changelog](https://github.com/ruby-syntax-tree/prettier_print/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ruby-syntax-tree/prettier_print/compare/v1.2.0...v1.2.1)

---
updated-dependencies:
- dependency-name: prettier_print
  dependency-type: indirect
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-10 10:11:48 +00:00
dependabot[bot]
29727408d6
Build(deps): Bump terser in /app/assets/javascripts (#20621)
Bumps [terser](https://github.com/terser/terser) from 5.16.5 to 5.16.6.
- [Release notes](https://github.com/terser/terser/releases)
- [Changelog](https://github.com/terser/terser/blob/master/CHANGELOG.md)
- [Commits](https://github.com/terser/terser/compare/v5.16.5...v5.16.6)

---
updated-dependencies:
- dependency-name: terser
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-10 10:11:27 +00:00
Alan Guo Xiang Tan
0d9efa938b
DEV: Avoid logging routing errors (#20622)
The logs are usually caused by the client and is of no use to us.
2023-03-10 17:17:59 +08:00
Ted Johansson
87ec058b8b
FEATURE: Configurable auto-bump cooldown (#20507)
Currently the auto-bump cooldown is hard-coded to 24 hours.

This change makes the highlighted 24 hours part configurable (defaulting to 24 hours), and the rest of the process remains the same.

This uses the new CategorySetting model associated with Category. We decided to add this because we want to move away from custom fields due to the lack of type casting and validations, but we want to keep the loading of these optional as they are not needed for almost all of the flows.

Category settings will be back-filled to all categories as part of this change, and creating a new category will now also create a category setting.
2023-03-10 13:45:01 +08:00
Martin Brennan
168de52538
DEV: Change sidebar header dropdown to use wait_for_animation (#20627)
* DEV: Change sidebar header dropdown to use wait_for_animation

Introduced in 54351e1b8a, this
helper should remove the need to have to add the .animated
CSS class in JS for the sidebar.

* DEV: Revert spec change
2023-03-10 14:54:57 +10:00
Sam
5893ad46ba
Revert "FIX: tag dropdown not working with default_list_filter (#20608)" (#20631)
This reverts commit 0df7743d78.

This causes too much instability in the unit test system, reverting
2023-03-10 13:45:34 +11:00
Osama Sayegh
118ce348f4
DEV: Let unread topics come through to /new when new new view is enabled (#20628)
Currently, if a user has opted into the new new experiment (introduced in a509441) and they click on the "See # new or updated topics" banner (screenshot below) at the top of the /new topics list, only new topics are loaded even if there are tracked topics with new replies.

This is unexpected in the new new view experiment because /new in this experiment is supposed to show both new and unread topics so it should listen for both new topics and new replies for existing/tracked topics. This PR addresses this inconsistency and makes it so that clicking the banner load all new and updated topics.
2023-03-10 09:57:35 +08:00
Sam
0df7743d78
FIX: tag dropdown not working with default_list_filter (#20608)
If you set a category to `default_list_filter` none. Information
was not passed to the tag route and routing was incorrect.

This patch fails, cause on reload route does not point to the right place.


```
-- a/app/assets/javascripts/discourse/app/routes/tag-show.js
+++ b/app/assets/javascripts/discourse/app/routes/tag-show.js
@@ -89,6 +89,8 @@ export default DiscourseRoute.extend(FilterModeMixin, {
       filter = `tag/${tagId}/l/${topicFilter}`;
     }
     const list = await findTopicList(
       this.store,
       this.topicTrackingState,
@@ -123,7 +125,7 @@ export default DiscourseRoute.extend(FilterModeMixin, {
   },

   setupController(controller, model) {
-    const noSubcategories =
+    this.noSubcategories =
       this.noSubcategories === undefined
         ? model.category?.default_list_filter === NONE
         : this.noSubcategories;
@@ -133,7 +135,7 @@ export default DiscourseRoute.extend(FilterModeMixin, {
       ...model,
       period: model.list.for_period,
       navMode: this.navMode,
-      noSubcategories,
+      noSubcategories: this.noSubcategories,
       loading: false,
     });
 ```

Long term we don't want to hide this logic from the routing (even in
the category case) it just cause unneeded confusion and fragility.
2023-03-10 11:37:55 +11:00
Michael Fitz-Payne
5624dbaa9b FIX(cache_critical_dns): use DB port sourced from environment
Fixes an assumption that the PostgreSQL port will always be reachable at
the discovered host on the default port when performing the healthcheck.

Instead we should be sourcing this from the same environment variable
that the application will be using to connect to.

Defaults to the standard PG port, 5432.
2023-03-10 10:09:07 +10:00
Selase Krakani
0603bd57df
FIX: Ensure form_template_ids is defined on new category records (#20610)
Navigating to the topic template tab on a new category form resulted in
exceptions because the `form_template_ids` property was undefined.

This fix sets the `form_template_ids` property on new category records.
2023-03-09 23:34:08 +00:00
Blake Erickson
2d0ad48dd1
Revert "DEV: Add crossOrigin to video tag (#20617)" (#20624)
This reverts commit f6063c684b.

Videos on sites with a cdn enabled aren't playing w/ a default cdn
config. They are showing a "CORS request did not succeed" error.
2023-03-09 16:20:35 -07:00
Jay Pfaffman
4350a903ec
clear security keys in disable_2fa rake task (#20586) 2023-03-09 15:38:59 -05:00
Blake Erickson
f6063c684b
DEV: Add crossOrigin to video tag (#20617)
* DEV: Add crossOrigin to video tag

This is a follow-up commit to f144c64e13
which enables the ability to generate thumbnail images for video
uploads.

In order for the html5 canvas element to create an image or blob the
source video element needs to to have the crossOrigin attribute set to
"anonymous" because a cdn is likely being used in production
environments.

We are already doing something similar in
e292c45924/app/assets/javascripts/discourse/app/lib/update-tab-count.js (L63)
2023-03-09 13:19:19 -07:00
Andrei Prigorshnev
e292c45924
DEV: better split create_notification! and send_notifications logic (#20562)
`create_notification!` - creates a notification in the database, `send_notifications` sends desktop and mobile notifications. This PR moves some code to decouple these two tasks more explicitly. It only moves code without changing any behavior, and the job is covered with tests (see chat_notify_mentioned_spec).
2023-03-09 22:17:18 +04:00
Joffrey JAFFEUX
73be7b3dd8
FIX: improves unread state precision (#20615)
- Will consider a message read only one the bottom of the message has been read
- Will allow to mark a message bigger than the view port as read
- Code should be more performant as the scroll is doing less (albeit more often)
- Gives us a very precise scroll state. Problem with throttling scroll is that you could end up never getting the even where scrollTop is at 0, opening a whole range of edge cases to handle
2023-03-09 19:06:33 +01:00
David Taylor
4f9afabf87
DEV: Resolve avatar-selector computed-property.override deprecation (#20616) 2023-03-09 18:05:46 +00:00
Blake Erickson
f144c64e13
Generate thumbnail images for video uploads (#19801)
* FEATURE: Generate thumbnail images for uploaded videos

Topics in Discourse have a topic thumbnail feature which allows themes
to show a preview image before viewing the actual Topic.

This PR allows for the ability to generate a thumbnail image from an
uploaded video that can be use for the topic preview.
2023-03-09 09:26:47 -07:00
chapoi
dd07e0dbd0
FIX: review q issues (#20558)
* DEV: specify type of flag in status

* FIX: passing missing parameter

* DEV: pass type for reviewable score table

* UX: add missing queued-topic styling

* UX: fix img overflow

* UX: add styling for queued user

* UX: fix user flag color

* UX: prevent overflow

* UX: add copy for filters

* FIX: fix typo in css for akismet flagging

* UX: copy change for flag something else

* UX: prevent overflow

* Fixing reviewable-status css classes

* Changes based on no longer using humanType

* Need to use type rather than humanType for reviewable-status

* FIX: linting

---------

Co-authored-by: Martin Brennan <martin@discourse.org>
2023-03-09 16:02:13 +01:00
Penar Musaraj
673cd4196f
FIX: Don't send image sizes for emojis/avatars (#20589)
When using the "review media unless trust level" setting, posts with emojis or quotes will end up in the review queue even though they don't have any uploaded media. That is because our heuristic for this in the new post manager relies on image_sizes. This commit skips sending image_sizes for emojis and avatars. 

Co-authored-by: Régis Hanol <regis@hanol.fr>
2023-03-09 09:53:27 -05:00
Kris
3cab9d5f80
UX: position of group user table dropdown, border (#20593) 2023-03-09 09:39:02 -05:00
David Taylor
b1727d9748
DEV: Resolve topic#details computed-property.override deprecation (#20612) 2023-03-09 13:44:50 +00:00
David Taylor
059ac3d31a
DEV: Unsilence and resolve setting-on-hash deprecation (#20611)
Select-kit was mutating a passed-in options hash to apply its own deprecations. This commit updates it to apply deprecated changes to the downstream `this.selectKit.options` object instead.
2023-03-09 13:44:31 +00:00
Jarek Radosz
3c4bfb6a9f
UX: Tweak last-visit/date separators (#20601)
1. Restore the left margin on both (which reflects the right margin of the scroll bar space)
2. Fix the center alignment of scroll-to-bottom icon
3. Fix the spacing of the `-` character between a date label and "last visit" label
4. Fix the incorrect display the border on date label when at the bottom of viewport
2023-03-09 13:44:02 +01:00
David Taylor
a99218677f
DEV: Unsilence two ember deprecations (#20609)
Neither of these are triggered in the core test suite. Unsilence so that plugins/themes receive deprecation notices.
2023-03-09 12:24:15 +00:00
Selase Krakani
ec40693f89
FIX: Ensure required_tag_group is defined on new category records (#20600)
Attaching required tag groups to new categories failed because
`required_tag_group` was undefined on the new category records

This fix sets an empty `required_tag_group` property on the new category
records.
2023-03-09 11:57:37 +00:00
Martin Brennan
ba1b95c9f4
FIX: Uploading multiple files to chat could cause canellations (#20605)
When we introduced `existingUploads` as an arg to the
ChatComposerUploads component, we also introduced a bug where
if multiple uploads were being done at once, and the draft
was saved, then because of didReceiveAttrs we would cancel
the currently uploading files because the draft uploads became
the existingUploads.

To work around this, since we do want to keep this on didReceiveAttrs
for cases when the user opens a draft or edits another message,
the easiest thing to do is to just not save uploads into the chat
draft if there are still uploads in progress. That way only when
all uploads are complete do we make them a part of the draft.

There is a small risk that the user could do something to lose
their uploads in the draft, but it's a better gamble to have
that happen rather than in progress uploads to be cancelled
while the user is waiting for them to be done because of the
draft.

Also changes the uploads system spec back to the old way of
attaching multiple files since that is why it was failing.
2023-03-09 09:17:54 +01:00
Michael Fitz-Payne
f38779adf4 DEV(cache_critical_dns): improve error reporting for failures
There are two failure modes that can be expected - no target SRV DNS RRs
found or no healthy service available at target addresses. Prior to this
patch, there was no way to differentiate from log messages between the
two cases.

Introduce an EmptyCache exception which may be raised by either the
ResolverCache or HealthyCache. The exception message contains enough
information about where the exception occurred to troubleshoot issues.

An existing bug was fixed in this commit. Previously if a target address
changed during runtime, an old cached (healthy) address would be
returned.. The behaviour has been corrected to return the newly cached
address.
2023-03-09 14:30:44 +10:00
Martin Brennan
5ea89d1fcb
FIX: UploadReference order by tiebreaker for UploadSecurity (#20602)
Follow up to 4d2a95ffe6. Sometimes
due to the original UploadReference migration or other issues,
multiple UploadReference records can have the exact same
created_at date and time. To tiebreak and correct the SQL order
when this happens, we can add a secondary `id ASC` ordering
when we check for the first upload reference.
2023-03-09 11:52:26 +10:00
Martin Brennan
931eedeb66
Revert "FIX: more precise unread message detection (#20588)" (#20604)
This reverts commit d78fed7dc6.

Causing some issues with clearing unreads.
2023-03-09 11:03:33 +10:00
Krzysztof Kotlarek
22bccef8f4
FIX: set external flag before validation (#20599)
Previously, `before_save` callback was used but `before_validation` has to be used to set external flag.
2023-03-09 10:44:54 +11:00
dependabot[bot]
008f71b961
Build(deps): Bump parser from 3.2.1.0 to 3.2.1.1 (#20597)
Bumps [parser](https://github.com/whitequark/parser) from 3.2.1.0 to 3.2.1.1.
- [Release notes](https://github.com/whitequark/parser/releases)
- [Changelog](https://github.com/whitequark/parser/blob/master/CHANGELOG.md)
- [Commits](https://github.com/whitequark/parser/compare/v3.2.1.0...v3.2.1.1)

---
updated-dependencies:
- dependency-name: parser
  dependency-type: indirect
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-08 23:25:15 +01:00
dependabot[bot]
2b5698a6ca
Build(deps): Bump webpack in /app/assets/javascripts (#20598)
Bumps [webpack](https://github.com/webpack/webpack) from 5.75.0 to 5.76.0.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v5.75.0...v5.76.0)

---
updated-dependencies:
- dependency-name: webpack
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-08 23:24:51 +01:00
dependabot[bot]
5930744e1d
Build(deps): Bump google-protobuf from 3.22.0 to 3.22.1 (#20596)
Bumps [google-protobuf](https://github.com/protocolbuffers/protobuf) from 3.22.0 to 3.22.1.
- [Release notes](https://github.com/protocolbuffers/protobuf/releases)
- [Changelog](https://github.com/protocolbuffers/protobuf/blob/main/generate_changelog.py)
- [Commits](https://github.com/protocolbuffers/protobuf/compare/v3.22.0...v3.22.1)

---
updated-dependencies:
- dependency-name: google-protobuf
  dependency-type: indirect
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-08 23:24:10 +01:00
Alan Guo Xiang Tan
d5ebcc3309
DEV: Remove ruby-lsp from Gemfile (#20595) 2023-03-09 05:34:54 +08:00
Joffrey JAFFEUX
f76ea32246
FIX: broken sticky date on firefox (#20594) 2023-03-08 22:23:24 +01:00
Jarek Radosz
c25d79168b
UX: Fix chat-reply overflow (#20592) 2023-03-08 21:12:49 +01:00
Keegan George
a805013b86
DEV: Add new plugin outlet for items left of breadcrumb (#20591) 2023-03-08 11:35:28 -08:00
Kris
7b1eb080ba
UX: ensure header logo has dimensions, style clean-up (#20512) 2023-03-08 12:50:36 -05:00
Joffrey JAFFEUX
d78fed7dc6
FIX: more precise unread message detection (#20588) 2023-03-08 17:28:54 +01:00
Joffrey JAFFEUX
4f2dfd3857
FIX: correctly syncs current user message in multiple sessions (#20584) 2023-03-08 17:28:39 +01:00
Martin Brennan
54351e1b8a
DEV: Introduces a wait_for_animation system spec helper (#20573)
This is used when calling click_message_action_mobile to wait
for the message actions menu to finish animating up before
attempting to click on it using capybara. Without this, in
the time between capybara getting the x,y position of a menu
item to click on and the click being fired, the animating menu
can move that item out of the way.

With the new helper, we constantly compare x,y client rect positions
for the animating element and wait for them to stabilise. Once they
do, it means the animation is done, and it is safe to click on
anything within the element.

Re-enables mobile system specs for chat that were ignored because
of this.
2023-03-08 16:49:20 +01:00
Roman Rizzi
910bf74c2e
FIX: Display a proper error when user already exists and email addresses are hidden. (#20585)
Follow-up to #16703. Returning an empty response leads to a bad UX since the user
has no feedback about what happened.
2023-03-08 12:38:58 -03:00
Loïc Guitaut
27f7cf18b1 FIX: Don’t email suspended users from group PM
Currently, when a suspended user belongs to a group PM (private message
with more than two people in it) and a staff member sends a message to
this group PM, then the suspended user will receive an email.
This happens because a suspended user can only receive emails from staff
members. But in this case, this can be seen as a bug as the expected
behavior would be instead to not send any email to the suspended user. A
staff member can participate in active discussions like any other
member and so their messages in this context shouldn’t be treated
differently than the ones from regular users.

This patch addresses this issue by checking if a suspended user receives
a message from a group PM or not. If that’s the case then an email won’t
be sent no matter if the post originated from a staff member or not.
2023-03-08 15:53:53 +01:00
Joffrey JAFFEUX
7f486cbc9b
FIX: do not show infinite loading state on draft with new users (#20582) 2023-03-08 15:21:20 +01:00
Kris
c659540475
FEATURE: tooltip for disabled new topic button (#20561) 2023-03-08 09:14:53 -05:00
Selase Krakani
9ec657f1fd
DEV: Make global search context suggestion first (#20581)
Currently, the global search context suggestion("in all posts and topics") which
also doubles as the default context on pressing Enter is displayed as
the second item in the initial search options suggested.

This changes makes it the first item in the suggested options.
2023-03-08 13:31:25 +00:00
Gerhard Schlager
12436d054d
DEV: Remove badge_granted_title column from user_profiles (#20476)
That column is obsolete since we added the `granted_title_badge_id` column in 2019 (56d3e29a69). Having both columns can lead to inconsistencies (mostly due to old data from before 2019).

For example, `BadgeGranter.revoke_ungranted_titles!` doesn't work correctly if `badge_granted_title` is `false` while `granted_title_badge_id` points to the badge that is used as title.
2023-03-08 13:37:20 +01:00
dependabot[bot]
5fb2c1dde5
Build(deps): Bump sorbet-runtime from 0.5.10696 to 0.5.10705 (#20565)
Bumps [sorbet-runtime](https://github.com/sorbet/sorbet) from 0.5.10696 to 0.5.10705.
- [Release notes](https://github.com/sorbet/sorbet/releases)
- [Commits](https://github.com/sorbet/sorbet/commits)

---
updated-dependencies:
- dependency-name: sorbet-runtime
  dependency-type: indirect
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-08 10:47:01 +01:00
David Battersby
8eb2fa5fa9
FEATURE: add new tags from edit tag synonyms page (#20553)
Feature to allow adding new tags from the edit tag synonyms tag search field.
Previously new tags had to be created from the topic composer, and then added via the edit tag synonyms page.

/t/92741
2023-03-08 14:26:20 +08:00
Martin Brennan
6feb436303
DEV: Change external upload rate limit maximums to settings (#20577)
Way back when this was introduced way back in b96c10a903
I didn't have any frame of reference for what these max rate
limit numbers should be, so 10 seemed like a reasonable limit
until a real world case where this did not make sense came
along.

The time has come.

Moving these into site settings, which are hidden since in most
cases there is no need to change these.
2023-03-08 15:27:17 +10:00
Martin Brennan
b62d44b40a
DEV: Fix another chat bookmark spec (#20578)
Followup to a252022117
2023-03-08 14:55:14 +10:00
Alan Guo Xiang Tan
cf0a0945e4
Revert "DEV: Allow webmock to intercept FinalDestination::HTTP requests (#20575)" (#20576) 2023-03-08 11:26:32 +08:00
Alan Guo Xiang Tan
500d0f6daf
DEV: Allow webmock to intercept FinalDestination::HTTP requests (#20575) 2023-03-08 10:40:01 +08:00
Martin Brennan
a252022117
DEV: Fix broken plugin specs because of bookmarkable changes (#20574) 2023-03-08 10:39:51 +08:00
Alan Guo Xiang Tan
b5cd22edb6
DEV: Introduce stub_ip_lookup spec helper (#20571) 2023-03-08 09:28:09 +08:00
Sam
3f5fa4eb09
DEV: avoid mocking FinalDestination (#20570) 2023-03-08 09:09:18 +08:00
Krzysztof Kotlarek
f2476d4b80
FIX: class for section link when name has space (#20569)
Sidebar section link has class name based on link title. Title can contain spaces, therefore they should be replaced.
2023-03-08 12:07:03 +11:00
Martin Brennan
360d0dde65
DEV: Change Bookmarkable registration to DiscoursePluginRegistry (#20556)
Similar spirit to e195e6f614,
this moves the Bookmarkable registration to DiscoursePluginRegistry
so plugins which are not enabled do not register additional
bookmarkable classes.
2023-03-08 10:39:12 +10:00
Krzysztof Kotlarek
1c881c1037
FIX: anonymous FAQ link to external URL (#20568)
When FAQ url is set to external resource, site is failing for anonymous user.
2023-03-08 11:21:02 +11:00
Alan Guo Xiang Tan
5e0c95ed83
PERF: Remove request for PM topic tracking state initiated from sidebar (#20554)
What is the problem?

When constructing the "Messages" section in Sidebar, we call
startTracking() on the pm-topic-tracking-state service in order to
get the counts for new/unread for the private message inboxes for each
user. However, this is unnecessary because the inboxes are in a
collapsed state by default in the sidebar and are only expanded when the
current route correspond to the inbox's route. Therefore, we can avoid
calling startTracking() on the pm-topic-tracking-state service until
an inbox's route is loaded. This allows us to cut out one extra request
to the server on page load and defer it until it is necessarily.
2023-03-08 07:07:23 +08:00
Joffrey JAFFEUX
2781264711
PERF: various perf improvements of chat-live-pane (#20563) 2023-03-07 18:55:05 +01:00
Andrei Prigorshnev
fa543cda06
DEV: Always create chat mention records (#20470)
Before this commit, we created a chat mention record only in case we wanted to send a notification about that mention to the user. Notifications were the only use case for the chat_mention db table. Now we want to use that table for other features, so we have to always create a chat_mention record.
2023-03-07 19:07:11 +04:00
Discourse Translator Bot
1f88354c5e
Update translations (#20559) 2023-03-07 14:58:31 +01:00
Ted Johansson
fdcb429145
FIX: Handle null values in category settings relative time pickers (#20552)
As reported on Meta, the relative time pickers for configuring slow-mode and auto-close durations in category settings are initially showing a "mins" option, which then disappears after you select any other timescale.

Our `RelativeTimePicker` component wasn't equipped to handle `null` values as the initial input. This caused it to go into a code path that set the selected timescale to "mins", even if that is not an allowed option.

There are two things being done here:

1. Add support for `null` input values to `RelativeTimePicker`. This fixes the auto-close setting.
2. Allow minutes for the slow-mode setting. (The user in Meta mentioned they usually set 15-30 minutes to cool down hot topics.
2023-03-07 11:05:13 +08:00
Krzysztof Kotlarek
a16ea24461
FEATURE: allow external links in custom sidebar sections (#20503)
Originally, only Discourse site links were available. After feedback, it was decided to extend this feature to external URLs.

/t/93491
2023-03-07 11:47:18 +11:00
Blake Erickson
b4528b9e27
FIX: Trim whitespace on email field for invites (#20547)
If the whitespace isn't trimmed from the input field the email is
considered invalid, and the button remains greyed out. We should handle
removing any trailing whitespace and not rely on the user trying to see
it themselves.
2023-03-06 17:39:59 -07:00
Keegan George
15cf62411a
DEV: Add /theme-qunit to skipped mini profiler paths (#20551) 2023-03-06 14:56:27 -08:00
Joffrey JAFFEUX
c52570ddc4
FIX: prevents mouseover to gain focus on sk row (#20550)
This was causing unattended effects on other elements. eg: the select-kit header input could lose focus when the list filtered would change size and cause the cursor to be positioned over a row.
2023-03-07 09:27:38 +11:00
dependabot[bot]
5f57b4b5aa
Build(deps): Bump rubocop from 1.47.0 to 1.48.0 (#20548)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-07 06:26:25 +08:00
Kris
6fc2cded55
UX: fix width for top embedded reply, post notice (#20546) 2023-03-06 11:24:49 -05:00
Joffrey JAFFEUX
e27c045f75
PERF: reduce height of the load more past message area (#20545)
On fast networks it could end up in infinite loop until all messages had been loaded.
2023-03-06 17:23:32 +01:00
Penar Musaraj
420214fc82
FIX: Deleting security keys was not working (#20427)
Bug was introduced in e313190fdb. There is
a workaround, using the trash icon in the edit modal, but that UI is
quite confusing to users.
2023-03-06 10:51:39 -05:00
Joffrey JAFFEUX
b5e736504a
PERF: applies optimisations on chat-live pane (#20532)
- group writes when computing separators positions
- shows skeleton only on initial load
- forces date separator to be pinned when first message to prevent a pinned - not pinned - pinned sequence when loading more in past
- relies on `message.visible` property instead of checking `isElementInViewport`
- attempts to load next/prev messages earlier
- do not scroll to on fetch more
- hides `last visit` text while pinned
2023-03-06 16:42:11 +01:00
Kris
d28390054e
UX: style improvements to new user tables (#20530) 2023-03-06 09:30:48 -05:00
David Taylor
62dc37ac35
DEV: Improve mini_profiler skipped paths (#20544)
- Remove no-longer-used `commits-widget` and `site_customizations` paths
- Add `/presence/`
- Move from regex to strings. The unanchored regexes were causing unexpected behaviour (e.g. if a topic had the word `assets` in its slug, it would be skipped). When passed strings, mini-profiler uses `String#starts_with?` for comparison
- Add `Discourse.base_path` to ensure proper functionality in subfolder environments
2023-03-06 11:39:15 +00:00
David Taylor
41f933ce89
PERF: Skip metadata routes for mini_profiler (#20543)
Mini-profiler causes routes to become uncacheable. When using mini_profiler in production, the lack of cache on these routes can have a noticeable impact on performance/rate-limiting.
2023-03-06 11:08:32 +00:00
Joffrey JAFFEUX
9e49abc0b9
FIX: do not refresh when accessing loaded reply (#20526)
Note this test my prove to be flakey, so I might have to remove it or find a different solution. It's extremely complicated to test for something which shouldn't appear in a period of time and is not a present at T=0
2023-03-06 10:09:21 +01:00
chapoi
28f8bdf91d
UX: remove visual chat msg staging effect (#20542) 2023-03-06 10:07:46 +01:00
Osama Sayegh
3f908c047d
FIX: Use the default value correctly for theme settings of type uploads (#20541)
When a theme setting of type `upload` has a default upload, it should return the URL of the specified default upload until a custom upload is used for the setting. However, currently this isn't the case and we get null instead of the default upload URL.

The reason for this is because the `super` method of `#value` already returns the default upload URL (if there's one), so we can't pass that to `cdn_url` which expects an upload ID:

c961dcc757/lib/theme_settings_manager.rb (L212)

This commit fixes the bug by skipping the call to `cdn_url` when we fallback to the default upload for the setting value.
2023-03-06 11:41:47 +03:00
Sam
c961dcc757
FIX: leaking callbacks to synchronize state (#20540)
Every time we created a topic list we would leak a state change callback

This happens on any topic list -> topic -> topic list sequence

This can cause corruption of tracking state and memory bloating given that
all information may be sent to the sync function.
2023-03-06 12:52:24 +08:00
dependabot[bot]
bfd4bfb824
Build(deps): Bump ruby-progressbar from 1.12.0 to 1.13.0 (#20533)
Bumps [ruby-progressbar](https://github.com/jfelchner/ruby-progressbar) from 1.12.0 to 1.13.0.
- [Release notes](https://github.com/jfelchner/ruby-progressbar/releases)
- [Changelog](https://github.com/jfelchner/ruby-progressbar/blob/master/CHANGELOG.md)
- [Commits](https://github.com/jfelchner/ruby-progressbar/compare/releases/v1.12.0...releases/v1.13.0)

---
updated-dependencies:
- dependency-name: ruby-progressbar
  dependency-type: indirect
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-06 11:52:37 +08:00
dependabot[bot]
36c8148377
Build(deps): Bump rack from 2.2.6.2 to 2.2.6.3 (#20535)
Bumps [rack](https://github.com/rack/rack) from 2.2.6.2 to 2.2.6.3.
- [Release notes](https://github.com/rack/rack/releases)
- [Changelog](https://github.com/rack/rack/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rack/rack/compare/v2.2.6.2...v2.2.6.3)

---
updated-dependencies:
- dependency-name: rack
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-06 11:52:15 +08:00
dependabot[bot]
a4e27da9e6
Build(deps): Bump sorbet-runtime from 0.5.10693 to 0.5.10696 (#20534)
Bumps [sorbet-runtime](https://github.com/sorbet/sorbet) from 0.5.10693 to 0.5.10696.
- [Release notes](https://github.com/sorbet/sorbet/releases)
- [Commits](https://github.com/sorbet/sorbet/commits)

---
updated-dependencies:
- dependency-name: sorbet-runtime
  dependency-type: indirect
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-06 11:51:57 +08:00
dependabot[bot]
a89ed6b14a
Build(deps): Bump faraday-retry from 2.0.0 to 2.1.0 (#20536)
Bumps [faraday-retry](https://github.com/lostisland/faraday-retry) from 2.0.0 to 2.1.0.
- [Release notes](https://github.com/lostisland/faraday-retry/releases)
- [Changelog](https://github.com/lostisland/faraday-retry/blob/main/CHANGELOG.md)
- [Commits](https://github.com/lostisland/faraday-retry/compare/v2.0.0...v2.1.0)

---
updated-dependencies:
- dependency-name: faraday-retry
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-06 11:51:33 +08:00
dependabot[bot]
d694cc8109
Build(deps-dev): Bump syntax_tree from 6.0.1 to 6.0.2 (#20537)
Bumps [syntax_tree](https://github.com/kddnewton/syntax_tree) from 6.0.1 to 6.0.2.
- [Release notes](https://github.com/kddnewton/syntax_tree/releases)
- [Changelog](https://github.com/ruby-syntax-tree/syntax_tree/blob/main/CHANGELOG.md)
- [Commits](https://github.com/kddnewton/syntax_tree/compare/v6.0.1...v6.0.2)

---
updated-dependencies:
- dependency-name: syntax_tree
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-06 11:49:15 +08:00
dependabot[bot]
9a58a97c45
Build(deps-dev): Bump minitest from 5.17.0 to 5.18.0 (#20538)
Bumps [minitest](https://github.com/seattlerb/minitest) from 5.17.0 to 5.18.0.
- [Release notes](https://github.com/seattlerb/minitest/releases)
- [Changelog](https://github.com/minitest/minitest/blob/master/History.rdoc)
- [Commits](https://github.com/seattlerb/minitest/compare/v5.17.0...v5.18.0)

---
updated-dependencies:
- dependency-name: minitest
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-06 11:49:01 +08:00
dependabot[bot]
08b2da4c1f
Build(deps): Bump msgpack from 1.6.0 to 1.6.1 (#20539)
Bumps [msgpack](https://github.com/msgpack/msgpack-ruby) from 1.6.0 to 1.6.1.
- [Release notes](https://github.com/msgpack/msgpack-ruby/releases)
- [Changelog](https://github.com/msgpack/msgpack-ruby/blob/master/ChangeLog)
- [Commits](https://github.com/msgpack/msgpack-ruby/commits)

---
updated-dependencies:
- dependency-name: msgpack
  dependency-type: indirect
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-06 11:48:41 +08:00
Alan Guo Xiang Tan
e3977f84a3
FIX: Incorrect topic tracking state count when a new category is created (#20506)
What is the problem?

We have a hidden site setting `show_category_definitions_in_topic_lists`
which is set to false by default. What this means is that category
definition topics are not shown in the topic list by default. Only the
category definition topic for the category being viewed will be shown.
However, we have a bug where we would show that a category has new
topics when a new child category along with its category definition
topic is created even though the topic list does not list the child
category's category definition topic.

What is the fix here?

This commit fixes the problem by shipping down an additional
`is_category_topic` attribute in `TopicTrackingStateItemSerializer` when
the `show_category_definitions_in_topic_lists` site setting has been set
to false. With the new attribute, we can then exclude counting child
categories' category definition topics when counting new and unread
counts for a category.
2023-03-06 10:13:10 +08:00
Blake Erickson
2fcfaccb5c
FIX: The default inbox label if username is not all lower cased in the url (#20531) 2023-03-05 10:04:56 -07:00
Joffrey JAFFEUX
1c9f300896
DEV: removes dead code (#20529) 2023-03-03 22:07:40 +01:00
Kris
26f77f03d5
UX: remove old group directory template, CSS (#20528) 2023-03-03 15:25:35 -05:00
Joffrey JAFFEUX
cdcd20fe1e
FIX: prevents duplicate reactions (#20527)
This was possible due to specific events which are hard to represent in a test. The provided test is as close as possible to what was happening in production: a message bus event was played on a channel which has just loaded its state with the existing reaction.
2023-03-03 20:29:24 +01:00
Joffrey JAFFEUX
0dc9c6c96d
FIX: prevents exception on required login sites with chat (#20525)
On require login sites, the `site` is not setup and as a result `hashtag_configurations` was blank and causing an error when attempting to access `["chat-composer"]` on it.
2023-03-03 17:40:23 +01:00
Joffrey JAFFEUX
5d6e8fdff0
UX: makes last visit stand out less (#20524) 2023-03-03 15:55:22 +01:00
Osama Sayegh
aad0d5fcfb
DEV: Unify behavior of category and tag links in sidebar in new new view experiment (#20488)
Follow up to a509441148

This commit makes category and tag link in the sidebar consistent with the Everything link when the new New view experiment is enabled. In particular:

1. Category and tag links navigate to the per-category (or tag) `/new` view if there's at least one topic, and to `/latest` if there are no topics
2. Category and tag links only show the count of topics in `/new` without text
3. The Everything link navigates to the global `/new` view if there's at least one topic there, and to `/latest` if there are no topics in `/new`.

Internal topic: t/77234.
2023-03-03 17:52:02 +03:00
Joffrey JAFFEUX
6b0aeced7e
DEV: rework the chat-live-pane (#20519)
This PR is introducing glimmer usage in the chat-live-pane, for components but also for models. RestModel usage has been dropped in favor of native classes.

Other changes/additions in this PR:

sticky dates, scrolling will now keep the date separator of the current section at the top of the screen
better unread management, marking a channel as unread will correctly mark the correct message and not mark the whole channel as read. Tracking state will also now correctly return unread count and unread mentions.
adds an animation on bottom arrow
better scrolling behavior, we should now always correctly keep the scroll position while loading more
reactions are now more reactive, and will update their tooltip without needed to close/reopen it
skeleton has been improved with placeholder images and reactions
when making a reaction on the desktop message actions, the menu won't move anymore
simplify logic and stop maintaining a list of unloaded messages
2023-03-03 13:09:25 +01:00
David Taylor
e08a0b509d
DEV: Support @debounce decorator in native class syntax (#20521)
The implementation previously generated a descriptor with an `initializer()`, and bound the function to the `this` context of the initializer. In native class syntax, the initializer of a descriptor is only called once, with a `this` context of the constructor, not the instance.

This commit updates the implementation so that it generates the bound function on-demand using a getter. This is the same strategy employed by ember's built-in `@action` decorator.

Unfortunately, this use of a getter means that the `@observes` decorator does not support being directly chained to `@debounce`. It throws the error "`observer must be provided a function or an observer definition`". The workaround is to put the observer on its own function, which then calls the debounced function. Given that we're aiming to reduce our usage of `@observes`, we've accepted the need for this workaround rather than spending the time to patch the implementation of `@observes`.
2023-03-03 11:48:58 +00:00
Meghna
36ad653fa9
UX: fix banner overlapping issue at breakpoint of around 1260px width (#20463)
There seems to be a breakpoint around 1260px width. When the window is narrower than that breakpoint, the “new or updated topics” banner seems to overlap the list below it.
2023-03-03 11:16:25 +05:30
Alan Guo Xiang Tan
66c50547b4
DEV: Experimental /filter route to filter through topics (#20494)
This commit introduces an experimental `/filter` route which allows a
user to input a query string to filter through topics.

Internal Ref: /t/92833
2023-03-03 09:46:21 +08:00
Kris
e022a7adec
UX: update user chat preference link for new nav (#20518) 2023-03-03 06:50:47 +08:00
Alan Guo Xiang Tan
990b710ade
DEV: Add ruby_lsp gem to development (#20517)
In order to use the ruby-lsp vscode extension, the ruby_lsp gem needs to
be added to the project's Gemfile. That may soon change with
https://github.com/Shopify/vscode-ruby-lsp/pull/419 but this will do for
now.
2023-03-03 06:45:52 +08:00
1079 changed files with 23897 additions and 18493 deletions

View File

@ -159,6 +159,22 @@ jobs:
path: tmp/turbo_rspec_runtime.log
key: rspec-runtime-backend-core
- name: Run Zeitwerk check
if: matrix.build_type == 'backend'
env:
LOAD_PLUGINS: ${{ (matrix.target == 'plugins') && '1' || '0' }}
run: |
if ! bin/rails zeitwerk:check --trace; then
echo
echo "---------------------------------------------"
echo
echo "::error::'bin/rails zeitwerk:check' failed - the app will fail to boot with 'eager_load=true' (e.g. in production)."
echo "To reproduce locally, run 'bin/rails zeitwerk:check'."
echo "Alternatively, you can run your local server/tests with the 'DISCOURSE_ZEITWERK_EAGER_LOAD=1' environment variable."
echo
exit 1
fi
- name: Core RSpec
if: matrix.build_type == 'backend' && matrix.target == 'core'
run: bin/turbo_rspec --verbose
@ -182,11 +198,11 @@ jobs:
- name: Core System Tests
if: matrix.build_type == 'system' && matrix.target == 'core'
run: PARALLEL_TEST_PROCESSORS=1 bin/turbo_rspec --verbose spec/system
run: bin/rspec spec/system
- name: Plugin System Tests
if: matrix.build_type == 'system' && matrix.target == 'plugins'
run: LOAD_PLUGINS=1 PARALLEL_TEST_PROCESSORS=1 bin/turbo_rspec --verbose plugins/*/spec/system
run: LOAD_PLUGINS=1 bin/rspec plugins/*/spec/system
- name: Upload failed system test screenshots
uses: actions/upload-artifact@v3

View File

@ -18,7 +18,7 @@ else
# this allows us to include the bits of rails we use without pieces we do not.
#
# To issue a rails update bump the version number here
rails_version = "7.0.4.1"
rails_version = "7.0.4.3"
gem "actionmailer", rails_version
gem "actionpack", rails_version
gem "actionview", rails_version

View File

@ -17,25 +17,25 @@ GIT
GEM
remote: https://rubygems.org/
specs:
actionmailer (7.0.4.1)
actionpack (= 7.0.4.1)
actionview (= 7.0.4.1)
activejob (= 7.0.4.1)
activesupport (= 7.0.4.1)
actionmailer (7.0.4.3)
actionpack (= 7.0.4.3)
actionview (= 7.0.4.3)
activejob (= 7.0.4.3)
activesupport (= 7.0.4.3)
mail (~> 2.5, >= 2.5.4)
net-imap
net-pop
net-smtp
rails-dom-testing (~> 2.0)
actionpack (7.0.4.1)
actionview (= 7.0.4.1)
activesupport (= 7.0.4.1)
actionpack (7.0.4.3)
actionview (= 7.0.4.3)
activesupport (= 7.0.4.3)
rack (~> 2.0, >= 2.2.0)
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.2.0)
actionview (7.0.4.1)
activesupport (= 7.0.4.1)
actionview (7.0.4.3)
activesupport (= 7.0.4.3)
builder (~> 3.1)
erubi (~> 1.4)
rails-dom-testing (~> 2.0)
@ -44,15 +44,15 @@ GEM
actionview (>= 6.0.a)
active_model_serializers (0.8.4)
activemodel (>= 3.0)
activejob (7.0.4.1)
activesupport (= 7.0.4.1)
activejob (7.0.4.3)
activesupport (= 7.0.4.3)
globalid (>= 0.3.6)
activemodel (7.0.4.1)
activesupport (= 7.0.4.1)
activerecord (7.0.4.1)
activemodel (= 7.0.4.1)
activesupport (= 7.0.4.1)
activesupport (7.0.4.1)
activemodel (7.0.4.3)
activesupport (= 7.0.4.3)
activerecord (7.0.4.3)
activemodel (= 7.0.4.3)
activesupport (= 7.0.4.3)
activesupport (7.0.4.3)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 1.6, < 2)
minitest (>= 5.1)
@ -157,7 +157,7 @@ GEM
faraday-net_http (>= 2.0, < 3.1)
ruby2_keywords (>= 0.0.4)
faraday-net_http (3.0.2)
faraday-retry (2.0.0)
faraday-retry (2.1.0)
faraday (~> 2.0)
fast_blank (1.0.1)
fast_xs (0.8.0)
@ -167,10 +167,11 @@ GEM
gc_tracer (1.5.1)
globalid (1.1.0)
activesupport (>= 5.0)
google-protobuf (3.22.0)
google-protobuf (3.22.0-arm64-darwin)
google-protobuf (3.22.0-x86_64-darwin)
google-protobuf (3.22.0-x86_64-linux)
google-protobuf (3.22.2)
google-protobuf (3.22.2-aarch64-linux)
google-protobuf (3.22.2-arm64-darwin)
google-protobuf (3.22.2-x86_64-darwin)
google-protobuf (3.22.2-x86_64-linux)
guess_html_encoding (0.0.11)
hana (1.3.7)
hashdiff (1.0.1)
@ -218,7 +219,7 @@ GEM
logstash-event (1.2.02)
logstash-logger (0.26.1)
logstash-event (~> 1.2)
logster (2.11.4)
logster (2.12.2)
loofah (2.19.1)
crass (~> 1.0.2)
nokogiri (>= 1.5.9)
@ -239,10 +240,10 @@ GEM
mini_sql (1.4.0)
mini_suffix (0.3.3)
ffi (~> 1.9)
minitest (5.17.0)
minitest (5.18.0)
mocha (2.0.2)
ruby2_keywords (>= 0.0.5)
msgpack (1.6.0)
msgpack (1.6.1)
multi_json (1.15.0)
multi_xml (0.6.0)
mustache (1.1.1)
@ -311,10 +312,10 @@ GEM
parallel (1.22.1)
parallel_tests (4.2.0)
parallel
parser (3.2.1.0)
parser (3.2.1.1)
ast (~> 2.4.1)
pg (1.4.6)
prettier_print (1.2.0)
prettier_print (1.2.1)
progress (3.6.0)
pry (0.14.2)
coderay (~> 1.1)
@ -328,12 +329,12 @@ GEM
puma (6.1.1)
nio4r (~> 2.0)
racc (1.6.2)
rack (2.2.6.2)
rack (2.2.6.4)
rack-mini-profiler (3.0.0)
rack (>= 1.2.0)
rack-protection (3.0.5)
rack
rack-test (2.0.2)
rack-test (2.1.0)
rack (>= 1.3)
rails-dom-testing (2.0.3)
activesupport (>= 4.2.0)
@ -347,9 +348,9 @@ GEM
rails_multisite (4.0.1)
activerecord (> 5.0, < 7.1)
railties (> 5.0, < 7.1)
railties (7.0.4.1)
actionpack (= 7.0.4.1)
activesupport (= 7.0.4.1)
railties (7.0.4.3)
actionpack (= 7.0.4.3)
activesupport (= 7.0.4.3)
method_source
rake (>= 12.2)
thor (~> 1.0)
@ -390,7 +391,7 @@ GEM
rspec-html-matchers (0.10.0)
nokogiri (~> 1)
rspec (>= 3.0.0.a)
rspec-mocks (3.12.3)
rspec-mocks (3.12.4)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.12.0)
rspec-rails (6.0.1)
@ -411,7 +412,7 @@ GEM
rspec-core (>= 2.14)
rtlcss (0.2.0)
mini_racer (~> 0.6.3)
rubocop (1.47.0)
rubocop (1.48.1)
json (~> 2.3)
parallel (~> 1.10)
parser (>= 3.2.0.0)
@ -425,14 +426,14 @@ GEM
parser (>= 3.2.1.0)
rubocop-capybara (2.17.1)
rubocop (~> 1.41)
rubocop-discourse (3.1.0)
rubocop-discourse (3.2.0)
rubocop (>= 1.1.0)
rubocop-rspec (>= 2.0.0)
rubocop-rspec (2.18.1)
rubocop-rspec (2.19.0)
rubocop (~> 1.33)
rubocop-capybara (~> 2.17)
ruby-prof (1.6.1)
ruby-progressbar (1.12.0)
ruby-progressbar (1.13.0)
ruby-readability (0.7.0)
guess_html_encoding (>= 0.0.4)
nokogiri (>= 1.6.0)
@ -441,16 +442,16 @@ GEM
sanitize (6.0.1)
crass (~> 1.0.2)
nokogiri (>= 1.12.0)
sass-embedded (1.58.3)
sass-embedded (1.59.2)
google-protobuf (~> 3.21)
rake (>= 10.0.0)
sass-embedded (1.58.3-aarch64-linux-gnu)
sass-embedded (1.59.2-aarch64-linux-gnu)
google-protobuf (~> 3.21)
sass-embedded (1.58.3-arm64-darwin)
sass-embedded (1.59.2-arm64-darwin)
google-protobuf (~> 3.21)
sass-embedded (1.58.3-x86_64-darwin)
sass-embedded (1.59.2-x86_64-darwin)
google-protobuf (~> 3.21)
sass-embedded (1.58.3-x86_64-linux-gnu)
sass-embedded (1.59.2-x86_64-linux-gnu)
google-protobuf (~> 3.21)
selenium-webdriver (4.8.1)
rexml (~> 3.2, >= 3.2.5)
@ -477,7 +478,7 @@ GEM
sprockets (>= 3.0.0)
sshkey (2.0.0)
stackprof (0.2.23)
syntax_tree (6.0.1)
syntax_tree (6.0.2)
prettier_print (>= 1.2.0)
syntax_tree-disable_ternary (1.0.0)
test-prof (1.2.0)
@ -533,14 +534,14 @@ PLATFORMS
x86_64-linux
DEPENDENCIES
actionmailer (= 7.0.4.1)
actionpack (= 7.0.4.1)
actionview (= 7.0.4.1)
actionmailer (= 7.0.4.3)
actionpack (= 7.0.4.3)
actionview (= 7.0.4.3)
actionview_precompiler
active_model_serializers (~> 0.8.3)
activemodel (= 7.0.4.1)
activerecord (= 7.0.4.1)
activesupport (= 7.0.4.1)
activemodel (= 7.0.4.3)
activerecord (= 7.0.4.3)
activesupport (= 7.0.4.3)
addressable
annotate
aws-sdk-s3
@ -626,7 +627,7 @@ DEPENDENCIES
rack-protection
rails_failover
rails_multisite
railties (= 7.0.4.1)
railties (= 7.0.4.3)
rake
rb-fsevent
rbtrace

View File

@ -1,13 +1,13 @@
import RESTAdapter from "discourse/adapters/rest";
import RestAdapter from "discourse/adapters/rest";
export default RESTAdapter.extend({
jsonMode: true,
export default class ApiKey extends RestAdapter {
jsonMode = true;
basePath() {
return "/admin/api/";
},
}
apiNameFor() {
return "key";
},
});
}
}

View File

@ -1,11 +1,11 @@
import RestAdapter from "discourse/adapters/rest";
export default function buildPluginAdapter(pluginName) {
return RestAdapter.extend({
return class extends RestAdapter {
pathFor(store, type, findArgs) {
return (
"/admin/plugins/" + pluginName + this._super(store, type, findArgs)
"/admin/plugins/" + pluginName + super.pathFor(store, type, findArgs)
);
},
});
}
};
}

View File

@ -1,7 +1,7 @@
import RestAdapter from "discourse/adapters/rest";
export default RestAdapter.extend({
export default class CustomizationBase extends RestAdapter {
basePath() {
return "/admin/customize/";
},
});
}
}

View File

@ -1,7 +1,7 @@
import RestAdapter from "discourse/adapters/rest";
export default RestAdapter.extend({
export default class EmailStyle extends RestAdapter {
pathFor() {
return "/admin/customize/email_style";
},
});
}
}

View File

@ -1,7 +1,7 @@
import RestAdapter from "discourse/adapters/rest";
export default RestAdapter.extend({
export default class Embedding extends RestAdapter {
pathFor() {
return "/admin/customize/embedding";
},
});
}
}

View File

@ -1,7 +1,7 @@
import RestAdapter from "discourse/adapters/rest";
export default RestAdapter.extend({
export default class StaffActionLog extends RestAdapter {
basePath() {
return "/admin/logs/";
},
});
}
}

View File

@ -1,5 +1,5 @@
import RestAdapter from "discourse/adapters/rest";
export default RestAdapter.extend({
jsonMode: true,
});
export default class TagGroup extends RestAdapter {
jsonMode = true;
}

View File

@ -1,9 +1,10 @@
import RestAdapter from "discourse/adapters/rest";
export default RestAdapter.extend({
export default class Theme extends RestAdapter {
jsonMode = true;
basePath() {
return "/admin/";
},
}
afterFindAll(results) {
let map = {};
@ -20,7 +21,5 @@ export default RestAdapter.extend({
theme.set("parentThemes", mappedParents);
});
return results;
},
jsonMode: true,
});
}
}

View File

@ -1,7 +1,7 @@
import RESTAdapter from "discourse/adapters/rest";
import RestAdapter from "discourse/adapters/rest";
export default RESTAdapter.extend({
export default class WebHookEvent extends RestAdapter {
basePath() {
return "/admin/api/";
},
});
}
}

View File

@ -1,7 +1,7 @@
import RESTAdapter from "discourse/adapters/rest";
import RestAdapter from "discourse/adapters/rest";
export default RESTAdapter.extend({
export default class WebHook extends RestAdapter {
basePath() {
return "/admin/api/";
},
});
}
}

View File

@ -56,6 +56,7 @@
{{#if this.hasInactiveThemes}}
{{#each this.inactiveThemes as |theme|}}
<ThemesListItem
@classNames="inactive-theme"
@theme={{theme}}
@navigateToTheme={{action "navigateToTheme" theme}}
/>

View File

@ -2,18 +2,18 @@ import Controller from "@ember/controller";
import { action } from "@ember/object";
import { popupAjaxError } from "discourse/lib/ajax-error";
export default Controller.extend({
loading: false,
export default class AdminApiKeysIndexController extends Controller {
loading = false;
@action
revokeKey(key) {
key.revoke().catch(popupAjaxError);
},
}
@action
undoRevokeKey(key) {
key.undoRevoke().catch(popupAjaxError);
},
}
@action
loadMore() {
@ -35,5 +35,5 @@ export default Controller.extend({
.finally(() => {
this.set("loading", false);
});
},
});
}
}

View File

@ -1,37 +1,32 @@
import { equal } from "@ember/object/computed";
import Controller from "@ember/controller";
import I18n from "I18n";
import discourseComputed from "discourse-common/utils/decorators";
import { isBlank } from "@ember/utils";
import { popupAjaxError } from "discourse/lib/ajax-error";
import { action, get } from "@ember/object";
import { equal } from "@ember/object/computed";
import showModal from "discourse/lib/show-modal";
import { ajax } from "discourse/lib/ajax";
export default Controller.extend({
userModes: null,
scopeModes: null,
globalScopes: null,
scopes: null,
export default class AdminApiKeysNewController extends Controller {
userModes = [
{ id: "all", name: I18n.t("admin.api.all_users") },
{ id: "single", name: I18n.t("admin.api.single_user") },
];
scopeModes = [
{ id: "granular", name: I18n.t("admin.api.scopes.granular") },
{ id: "read_only", name: I18n.t("admin.api.scopes.read_only") },
{ id: "global", name: I18n.t("admin.api.scopes.global") },
];
globalScopes = null;
scopes = null;
@equal("userMode", "single") showUserSelector;
init() {
this._super(...arguments);
this.set("userModes", [
{ id: "all", name: I18n.t("admin.api.all_users") },
{ id: "single", name: I18n.t("admin.api.single_user") },
]);
this.set("scopeModes", [
{ id: "granular", name: I18n.t("admin.api.scopes.granular") },
{ id: "read_only", name: I18n.t("admin.api.scopes.read_only") },
{ id: "global", name: I18n.t("admin.api.scopes.global") },
]);
super.init(...arguments);
this._loadScopes();
},
showUserSelector: equal("userMode", "single"),
}
@discourseComputed("model.{description,username}", "showUserSelector")
saveDisabled(model, showUserSelector) {
@ -42,12 +37,12 @@ export default Controller.extend({
return true;
}
return false;
},
}
@action
updateUsername(selected) {
this.set("model.username", get(selected, "firstObject"));
},
}
@action
changeUserMode(userMode) {
@ -55,12 +50,12 @@ export default Controller.extend({
this.model.set("username", null);
}
this.set("userMode", userMode);
},
}
@action
changeScopeMode(scopeMode) {
this.set("scopeMode", scopeMode);
},
}
@action
save() {
@ -77,12 +72,12 @@ export default Controller.extend({
}
return this.model.save().catch(popupAjaxError);
},
}
@action
continue() {
this.transitionToRoute("adminApiKeys.show", this.model.id);
},
}
@action
showURLs(urls) {
@ -90,7 +85,7 @@ export default Controller.extend({
admin: true,
model: { urls },
});
},
}
_loadScopes() {
return ajax("/admin/api/keys/scopes.json")
@ -102,5 +97,5 @@ export default Controller.extend({
this.set("scopes", data.scopes);
})
.catch(popupAjaxError);
},
});
}
}

View File

@ -1,66 +1,74 @@
import { action } from "@ember/object";
import { empty } from "@ember/object/computed";
import Controller from "@ember/controller";
import { bufferedProperty } from "discourse/mixins/buffered-content";
import { empty } from "@ember/object/computed";
import { isEmpty } from "@ember/utils";
import { popupAjaxError } from "discourse/lib/ajax-error";
import showModal from "discourse/lib/show-modal";
export default Controller.extend(bufferedProperty("model"), {
isNew: empty("model.id"),
export default class AdminApiKeysShowController extends Controller.extend(
bufferedProperty("model")
) {
@empty("model.id") isNew;
actions: {
saveDescription() {
const buffered = this.buffered;
const attrs = buffered.getProperties("description");
@action
saveDescription() {
const buffered = this.buffered;
const attrs = buffered.getProperties("description");
this.model
.save(attrs)
.then(() => {
this.set("editingDescription", false);
this.rollbackBuffer();
})
.catch(popupAjaxError);
},
cancel() {
const id = this.get("userField.id");
if (isEmpty(id)) {
this.destroyAction(this.userField);
} else {
this.model
.save(attrs)
.then(() => {
this.set("editingDescription", false);
this.rollbackBuffer();
this.set("editing", false);
}
},
})
.catch(popupAjaxError);
}
editDescription() {
this.toggleProperty("editingDescription");
if (!this.editingDescription) {
this.rollbackBuffer();
}
},
@action
cancel() {
const id = this.get("userField.id");
if (isEmpty(id)) {
this.destroyAction(this.userField);
} else {
this.rollbackBuffer();
this.set("editing", false);
}
}
revokeKey(key) {
key.revoke().catch(popupAjaxError);
},
@action
editDescription() {
this.toggleProperty("editingDescription");
if (!this.editingDescription) {
this.rollbackBuffer();
}
}
deleteKey(key) {
key
.destroyRecord()
.then(() => this.transitionToRoute("adminApiKeys.index"))
.catch(popupAjaxError);
},
@action
revokeKey(key) {
key.revoke().catch(popupAjaxError);
}
undoRevokeKey(key) {
key.undoRevoke().catch(popupAjaxError);
},
@action
deleteKey(key) {
key
.destroyRecord()
.then(() => this.transitionToRoute("adminApiKeys.index"))
.catch(popupAjaxError);
}
showURLs(urls) {
return showModal("admin-api-key-urls", {
admin: true,
model: {
urls,
},
});
},
},
});
@action
undoRevokeKey(key) {
key.undoRevoke().catch(popupAjaxError);
}
@action
showURLs(urls) {
return showModal("admin-api-key-urls", {
admin: true,
model: {
urls,
},
});
}
}

View File

@ -1,3 +1,3 @@
import Controller from "@ember/controller";
export default Controller.extend();
export default class AdminApiKeysController extends Controller {}

View File

@ -1,19 +1,21 @@
import Controller, { inject as controller } from "@ember/controller";
import { action } from "@ember/object";
import { inject as service } from "@ember/service";
import { alias, equal } from "@ember/object/computed";
import Controller, { inject as controller } from "@ember/controller";
import { i18n, setting } from "discourse/lib/computed";
import I18n from "I18n";
import { ajax } from "discourse/lib/ajax";
import discourseComputed from "discourse-common/utils/decorators";
import { inject as service } from "@ember/service";
export default Controller.extend({
adminBackups: controller(),
dialog: service(),
status: alias("adminBackups.model"),
uploadLabel: i18n("admin.backups.upload.label"),
backupLocation: setting("backup_location"),
localBackupStorage: equal("backupLocation", "local"),
export default class AdminBackupsIndexController extends Controller {
@service dialog;
@controller adminBackups;
@alias("adminBackups.model") status;
@i18n("admin.backups.upload.label") uploadLabel;
@setting("backup_location") backupLocation;
@equal("backupLocation", "local") localBackupStorage;
@discourseComputed("status.allowRestore", "status.isOperationRunning")
restoreTitle(allowRestore, isOperationRunning) {
@ -24,35 +26,35 @@ export default Controller.extend({
} else {
return "admin.backups.operations.restore.title";
}
},
}
actions: {
toggleReadOnlyMode() {
if (!this.site.get("isReadOnly")) {
this.dialog.yesNoConfirm({
message: I18n.t("admin.backups.read_only.enable.confirm"),
didConfirm: () => {
this.set("currentUser.hideReadOnlyAlert", true);
this._toggleReadOnlyMode(true);
},
});
} else {
this._toggleReadOnlyMode(false);
}
},
@action
toggleReadOnlyMode() {
if (!this.site.get("isReadOnly")) {
this.dialog.yesNoConfirm({
message: I18n.t("admin.backups.read_only.enable.confirm"),
didConfirm: () => {
this.set("currentUser.hideReadOnlyAlert", true);
this._toggleReadOnlyMode(true);
},
});
} else {
this._toggleReadOnlyMode(false);
}
}
download(backup) {
const link = backup.get("filename");
ajax(`/admin/backups/${link}`, { type: "PUT" }).then(() =>
this.dialog.alert(I18n.t("admin.backups.operations.download.alert"))
);
},
},
@action
download(backup) {
const link = backup.get("filename");
ajax(`/admin/backups/${link}`, { type: "PUT" }).then(() =>
this.dialog.alert(I18n.t("admin.backups.operations.download.alert"))
);
}
_toggleReadOnlyMode(enable) {
ajax("/admin/backups/readonly", {
type: "PUT",
data: { enable },
}).then(() => this.site.set("isReadOnly", enable));
},
});
}
}

View File

@ -1,13 +1,10 @@
import Controller, { inject as controller } from "@ember/controller";
import { alias } from "@ember/object/computed";
import Controller, { inject as controller } from "@ember/controller";
export default Controller.extend({
adminBackups: controller(),
status: alias("adminBackups.model"),
export default class AdminBackupsLogsController extends Controller {
@controller adminBackups;
init() {
this._super(...arguments);
@alias("adminBackups.model") status;
this.logs = [];
},
});
logs = [];
}

View File

@ -1,11 +1,8 @@
import { and, not } from "@ember/object/computed";
import Controller from "@ember/controller";
export default Controller.extend({
noOperationIsRunning: not("model.isOperationRunning"),
rollbackEnabled: and(
"model.canRollback",
"model.restoreEnabled",
"noOperationIsRunning"
),
rollbackDisabled: not("rollbackEnabled"),
});
export default class AdminBackupsController extends Controller {
@not("model.isOperationRunning") noOperationIsRunning;
@not("rollbackEnabled") rollbackDisabled;
@and("model.canRollback", "model.restoreEnabled", "noOperationIsRunning")
rollbackEnabled;
}

View File

@ -1,19 +1,19 @@
import Controller from "@ember/controller";
import EmberObject from "@ember/object";
import EmberObject, { action } from "@ember/object";
import I18n from "I18n";
import discourseComputed from "discourse-common/utils/decorators";
import showModal from "discourse/lib/show-modal";
export default Controller.extend({
export default class AdminCustomizeColorsController extends Controller {
@discourseComputed("model.@each.id")
baseColorScheme() {
return this.model.findBy("is_base", true);
},
}
@discourseComputed("model.@each.id")
baseColorSchemes() {
return this.model.filterBy("is_base", true);
},
}
@discourseComputed("baseColorScheme")
baseColors(baseColorScheme) {
@ -22,28 +22,28 @@ export default Controller.extend({
baseColorsHash.set(color.get("name"), color);
});
return baseColorsHash;
},
}
actions: {
newColorSchemeWithBase(baseKey) {
const base = this.baseColorSchemes.findBy("base_scheme_id", baseKey);
const newColorScheme = base.copy();
newColorScheme.setProperties({
name: I18n.t("admin.customize.colors.new_name"),
base_scheme_id: base.get("base_scheme_id"),
});
newColorScheme.save().then(() => {
this.model.pushObject(newColorScheme);
newColorScheme.set("savingStatus", null);
this.replaceRoute("adminCustomize.colors.show", newColorScheme);
});
},
@action
newColorSchemeWithBase(baseKey) {
const base = this.baseColorSchemes.findBy("base_scheme_id", baseKey);
const newColorScheme = base.copy();
newColorScheme.setProperties({
name: I18n.t("admin.customize.colors.new_name"),
base_scheme_id: base.get("base_scheme_id"),
});
newColorScheme.save().then(() => {
this.model.pushObject(newColorScheme);
newColorScheme.set("savingStatus", null);
this.replaceRoute("adminCustomize.colors.show", newColorScheme);
});
}
newColorScheme() {
showModal("admin-color-scheme-select-base", {
model: this.baseColorSchemes,
admin: true,
});
},
},
});
@action
newColorScheme() {
showModal("admin-color-scheme-select-base", {
model: this.baseColorSchemes,
admin: true,
});
}
}

View File

@ -1,38 +1,38 @@
import { action } from "@ember/object";
import { inject as service } from "@ember/service";
import Controller from "@ember/controller";
import I18n from "I18n";
import discourseComputed from "discourse-common/utils/decorators";
import { inject as service } from "@ember/service";
export default Controller.extend({
dialog: service(),
export default class AdminCustomizeEmailStyleEditController extends Controller {
@service dialog;
@discourseComputed("model.isSaving")
saveButtonText(isSaving) {
return isSaving ? I18n.t("saving") : I18n.t("admin.customize.save");
},
}
@discourseComputed("model.changed", "model.isSaving")
saveDisabled(changed, isSaving) {
return !changed || isSaving;
},
}
actions: {
save() {
if (!this.model.saving) {
this.set("saving", true);
this.model
.update(this.model.getProperties("html", "css"))
.catch((e) => {
const msg =
e.jqXHR.responseJSON && e.jqXHR.responseJSON.errors
? I18n.t("admin.customize.email_style.save_error_with_reason", {
error: e.jqXHR.responseJSON.errors.join(". "),
})
: I18n.t("generic_error");
this.dialog.alert(msg);
})
.finally(() => this.set("model.changed", false));
}
},
},
});
@action
save() {
if (!this.model.saving) {
this.set("saving", true);
this.model
.update(this.model.getProperties("html", "css"))
.catch((e) => {
const msg =
e.jqXHR.responseJSON && e.jqXHR.responseJSON.errors
? I18n.t("admin.customize.email_style.save_error_with_reason", {
error: e.jqXHR.responseJSON.errors.join(". "),
})
: I18n.t("generic_error");
this.dialog.alert(msg);
})
.finally(() => this.set("model.changed", false));
}
}
}

View File

@ -1,23 +1,26 @@
import { inject as service } from "@ember/service";
import Controller, { inject as controller } from "@ember/controller";
import I18n from "I18n";
import { action } from "@ember/object";
import { bufferedProperty } from "discourse/mixins/buffered-content";
import discourseComputed from "discourse-common/utils/decorators";
import { popupAjaxError } from "discourse/lib/ajax-error";
import { inject as service } from "@ember/service";
export default Controller.extend(bufferedProperty("emailTemplate"), {
adminCustomizeEmailTemplates: controller(),
dialog: service(),
emailTemplate: null,
saved: false,
export default class AdminCustomizeEmailTemplatesEditController extends Controller.extend(
bufferedProperty("emailTemplate")
) {
@service dialog;
@controller adminCustomizeEmailTemplates;
emailTemplate = null;
saved = false;
@discourseComputed("buffered.body", "buffered.subject")
saveDisabled(body, subject) {
return (
this.emailTemplate.body === body && this.emailTemplate.subject === subject
);
},
}
@discourseComputed("buffered")
hasMultipleSubjects(buffered) {
@ -26,7 +29,7 @@ export default Controller.extend(bufferedProperty("emailTemplate"), {
} else {
return buffered.getProperties("id")["id"];
}
},
}
@action
saveChanges() {
@ -38,7 +41,7 @@ export default Controller.extend(bufferedProperty("emailTemplate"), {
this.set("saved", true);
})
.catch(popupAjaxError);
},
}
@action
revertChanges() {
@ -57,5 +60,5 @@ export default Controller.extend(bufferedProperty("emailTemplate"), {
.catch(popupAjaxError);
},
});
},
});
}
}

View File

@ -1,18 +1,13 @@
import { sort } from "@ember/object/computed";
import Controller from "@ember/controller";
import { action } from "@ember/object";
import { sort } from "@ember/object/computed";
export default Controller.extend({
sortedTemplates: sort("emailTemplates", "titleSorting"),
init() {
this._super(...arguments);
this.set("titleSorting", ["title"]);
},
export default class AdminCustomizeEmailTemplatesController extends Controller {
titleSorting = ["title"];
@sort("emailTemplates", "titleSorting") sortedTemplates;
@action
onSelectTemplate(template) {
this.transitionToRoute("adminCustomizeEmailTemplates.edit", template);
},
});
}
}

View File

@ -1,47 +1,52 @@
import { action } from "@ember/object";
import { not } from "@ember/object/computed";
import Controller from "@ember/controller";
import { ajax } from "discourse/lib/ajax";
import { bufferedProperty } from "discourse/mixins/buffered-content";
import { not } from "@ember/object/computed";
import { propertyEqual } from "discourse/lib/computed";
export default Controller.extend(bufferedProperty("model"), {
saved: false,
isSaving: false,
saveDisabled: propertyEqual("model.robots_txt", "buffered.robots_txt"),
resetDisabled: not("model.overridden"),
export default class AdminCustomizeRobotsTxtController extends Controller.extend(
bufferedProperty("model")
) {
saved = false;
isSaving = false;
actions: {
save() {
this.setProperties({
isSaving: true,
saved: false,
});
@propertyEqual("model.robots_txt", "buffered.robots_txt") saveDisabled;
ajax("robots.json", {
type: "PUT",
data: { robots_txt: this.buffered.get("robots_txt") },
@not("model.overridden") resetDisabled;
@action
save() {
this.setProperties({
isSaving: true,
saved: false,
});
ajax("robots.json", {
type: "PUT",
data: { robots_txt: this.buffered.get("robots_txt") },
})
.then((data) => {
this.commitBuffer();
this.set("saved", true);
this.set("model.overridden", data.overridden);
})
.then((data) => {
this.commitBuffer();
this.set("saved", true);
this.set("model.overridden", data.overridden);
})
.finally(() => this.set("isSaving", false));
},
.finally(() => this.set("isSaving", false));
}
reset() {
this.setProperties({
isSaving: true,
saved: false,
});
ajax("robots.json", { type: "DELETE" })
.then((data) => {
this.buffered.set("robots_txt", data.robots_txt);
this.commitBuffer();
this.set("saved", true);
this.set("model.overridden", false);
})
.finally(() => this.set("isSaving", false));
},
},
});
@action
reset() {
this.setProperties({
isSaving: true,
saved: false,
});
ajax("robots.json", { type: "DELETE" })
.then((data) => {
this.buffered.set("robots_txt", data.robots_txt);
this.commitBuffer();
this.set("saved", true);
this.set("model.overridden", false);
})
.finally(() => this.set("isSaving", false));
}
}

View File

@ -1,21 +1,24 @@
import { action } from "@ember/object";
import Controller from "@ember/controller";
import I18n from "I18n";
import discourseComputed from "discourse-common/utils/decorators";
import { url } from "discourse/lib/computed";
export default Controller.extend({
section: null,
currentTarget: 0,
maximized: false,
previewUrl: url("model.id", "/admin/themes/%@/preview"),
showAdvanced: false,
editRouteName: "adminCustomizeThemes.edit",
showRouteName: "adminCustomizeThemes.show",
export default class AdminCustomizeThemesEditController extends Controller {
section = null;
currentTarget = 0;
maximized = false;
@url("model.id", "/admin/themes/%@/preview") previewUrl;
showAdvanced = false;
editRouteName = "adminCustomizeThemes.edit";
showRouteName = "adminCustomizeThemes.show";
setTargetName(name) {
const target = this.get("model.targets").find((t) => t.name === name);
this.set("currentTarget", target && target.id);
},
}
@discourseComputed("currentTarget")
currentTargetName(id) {
@ -23,50 +26,52 @@ export default Controller.extend({
(t) => t.id === parseInt(id, 10)
);
return target && target.name;
},
}
@discourseComputed("model.isSaving")
saveButtonText(isSaving) {
return isSaving ? I18n.t("saving") : I18n.t("admin.customize.save");
},
}
@discourseComputed("model.changed", "model.isSaving")
saveDisabled(changed, isSaving) {
return !changed || isSaving;
},
}
actions: {
save() {
this.set("saving", true);
this.model.saveChanges("theme_fields").finally(() => {
this.set("saving", false);
});
},
@action
save() {
this.set("saving", true);
this.model.saveChanges("theme_fields").finally(() => {
this.set("saving", false);
});
}
fieldAdded(target, name) {
this.replaceRoute(this.editRouteName, this.get("model.id"), target, name);
},
@action
fieldAdded(target, name) {
this.replaceRoute(this.editRouteName, this.get("model.id"), target, name);
}
onlyOverriddenChanged(onlyShowOverridden) {
if (onlyShowOverridden) {
if (!this.model.hasEdited(this.currentTargetName, this.fieldName)) {
let firstTarget = this.get("model.targets").find((t) => t.edited);
let firstField = this.get(`model.fields.${firstTarget.name}`).find(
(f) => f.edited
);
@action
onlyOverriddenChanged(onlyShowOverridden) {
if (onlyShowOverridden) {
if (!this.model.hasEdited(this.currentTargetName, this.fieldName)) {
let firstTarget = this.get("model.targets").find((t) => t.edited);
let firstField = this.get(`model.fields.${firstTarget.name}`).find(
(f) => f.edited
);
this.replaceRoute(
this.editRouteName,
this.get("model.id"),
firstTarget.name,
firstField.name
);
}
this.replaceRoute(
this.editRouteName,
this.get("model.id"),
firstTarget.name,
firstField.name
);
}
},
}
}
goBack() {
this.replaceRoute(this.showRouteName, this.model.id);
},
},
});
@action
goBack() {
this.replaceRoute(this.showRouteName, this.model.id);
}
}

View File

@ -1,4 +1,4 @@
import { COMPONENTS, THEMES } from "admin/models/theme";
import { inject as service } from "@ember/service";
import {
empty,
filterBy,
@ -6,8 +6,9 @@ import {
match,
notEmpty,
} from "@ember/object/computed";
import { COMPONENTS, THEMES } from "admin/models/theme";
import Controller from "@ember/controller";
import EmberObject from "@ember/object";
import EmberObject, { action } from "@ember/object";
import I18n from "I18n";
import ThemeSettings from "admin/models/theme-settings";
import discourseComputed from "discourse-common/utils/decorators";
@ -15,31 +16,35 @@ import { makeArray } from "discourse-common/lib/helpers";
import { popupAjaxError } from "discourse/lib/ajax-error";
import showModal from "discourse/lib/show-modal";
import { url } from "discourse/lib/computed";
import { inject as service } from "@ember/service";
const THEME_UPLOAD_VAR = 2;
export default Controller.extend({
dialog: service(),
downloadUrl: url("model.id", "/admin/customize/themes/%@/export"),
previewUrl: url("model.id", "/admin/themes/%@/preview"),
addButtonDisabled: empty("selectedChildThemeId"),
editRouteName: "adminCustomizeThemes.edit",
parentThemesNames: mapBy("model.parentThemes", "name"),
availableParentThemes: filterBy("allThemes", "component", false),
availableActiveParentThemes: filterBy("availableParentThemes", "isActive"),
availableThemesNames: mapBy("availableParentThemes", "name"),
availableActiveThemesNames: mapBy("availableActiveParentThemes", "name"),
availableActiveChildThemes: filterBy("availableChildThemes", "hasParents"),
availableComponentsNames: mapBy("availableChildThemes", "name"),
availableActiveComponentsNames: mapBy("availableActiveChildThemes", "name"),
childThemesNames: mapBy("model.childThemes", "name"),
extraFiles: filterBy("model.theme_fields", "target", "extra_js"),
export default class AdminCustomizeThemesShowController extends Controller {
@service dialog;
editRouteName = "adminCustomizeThemes.edit";
@url("model.id", "/admin/customize/themes/%@/export") downloadUrl;
@url("model.id", "/admin/themes/%@/preview") previewUrl;
@empty("selectedChildThemeId") addButtonDisabled;
@mapBy("model.parentThemes", "name") parentThemesNames;
@filterBy("allThemes", "component", false) availableParentThemes;
@filterBy("availableParentThemes", "isActive") availableActiveParentThemes;
@mapBy("availableParentThemes", "name") availableThemesNames;
@mapBy("availableActiveParentThemes", "name") availableActiveThemesNames;
@filterBy("availableChildThemes", "hasParents") availableActiveChildThemes;
@mapBy("availableChildThemes", "name") availableComponentsNames;
@mapBy("availableActiveChildThemes", "name") availableActiveComponentsNames;
@mapBy("model.childThemes", "name") childThemesNames;
@filterBy("model.theme_fields", "target", "extra_js") extraFiles;
@notEmpty("settings") hasSettings;
@notEmpty("translations") hasTranslations;
@match("model.remote_theme.remote_url", /^http(s)?:\/\//) sourceIsHttp;
@discourseComputed("model.component", "model.remote_theme")
showCheckboxes() {
return !this.model.component || this.model.remote_theme;
},
}
@discourseComputed("model.editedFields")
editedFieldsFormatted() {
@ -57,13 +62,13 @@ export default Controller.extend({
descriptions.push(resultString);
});
return descriptions;
},
}
@discourseComputed("colorSchemeId", "model.color_scheme_id")
colorSchemeChanged(colorSchemeId, existingId) {
colorSchemeId = colorSchemeId === null ? null : parseInt(colorSchemeId, 10);
return colorSchemeId !== existingId;
},
}
@discourseComputed("availableChildThemes", "model.childThemes.[]", "model")
selectableChildThemes(available, childThemes) {
@ -73,7 +78,7 @@ export default Controller.extend({
: available.filter((theme) => !childThemes.includes(theme));
return themes.length === 0 ? null : themes;
}
},
}
@discourseComputed("model.parentThemes.[]")
relativesSelectorSettingsForComponent() {
@ -91,7 +96,7 @@ export default Controller.extend({
allThemes: this.allThemes,
setDefaultValuesLabel: I18n.t("admin.customize.theme.add_all_themes"),
});
},
}
@discourseComputed("model.parentThemes.[]")
relativesSelectorSettingsForTheme() {
@ -109,7 +114,7 @@ export default Controller.extend({
allThemes: this.allThemes,
setDefaultValuesLabel: I18n.t("admin.customize.theme.add_all"),
});
},
}
@discourseComputed("allThemes", "model.component", "model")
availableChildThemes(allThemes) {
@ -119,40 +124,36 @@ export default Controller.extend({
(theme) => theme.get("id") !== themeId && theme.get("component")
);
}
},
}
@discourseComputed("model.component")
convertKey(component) {
const type = component ? "component" : "theme";
return `admin.customize.theme.convert_${type}`;
},
}
@discourseComputed("model.component")
convertIcon(component) {
return component ? "cube" : "";
},
}
@discourseComputed("model.component")
convertTooltip(component) {
const type = component ? "component" : "theme";
return `admin.customize.theme.convert_${type}_tooltip`;
},
}
@discourseComputed("model.settings")
settings(settings) {
return settings.map((setting) => ThemeSettings.create(setting));
},
hasSettings: notEmpty("settings"),
}
@discourseComputed("model.translations")
translations(translations) {
return translations.map((setting) =>
ThemeSettings.create({ ...setting, textarea: true })
);
},
hasTranslations: notEmpty("translations"),
}
@discourseComputed(
"model.remote_theme.local_version",
@ -161,12 +162,12 @@ export default Controller.extend({
)
hasOverwrittenHistory(localVersion, remoteVersion, commitsBehind) {
return localVersion !== remoteVersion && commitsBehind === -1;
},
}
@discourseComputed("model.remoteError", "updatingRemote")
showRemoteError(errorMessage, updating) {
return errorMessage && !updating;
},
}
@discourseComputed(
"model.remote_theme.remote_url",
@ -175,13 +176,13 @@ export default Controller.extend({
)
finishInstall(remoteUrl, localVersion, commitsBehind) {
return remoteUrl && !localVersion && !commitsBehind;
},
}
editedFieldsForTarget(target) {
return this.get("model.editedFields").filter(
(field) => field.target === target
);
},
}
commitSwitchType() {
const model = this.model;
@ -222,7 +223,8 @@ export default Controller.extend({
});
})
.catch(popupAjaxError);
},
}
transitionToEditRoute() {
this.transitionToRoute(
this.editRouteName,
@ -230,8 +232,7 @@ export default Controller.extend({
"common",
"scss"
);
},
sourceIsHttp: match("model.remote_theme.remote_url", /^http(s)?:\/\//),
}
@discourseComputed(
"model.remote_theme.remote_url",
@ -241,168 +242,186 @@ export default Controller.extend({
return remoteThemeBranch
? `${remoteThemeUrl.replace(/\.git$/, "")}/tree/${remoteThemeBranch}`
: remoteThemeUrl;
},
}
@discourseComputed("model.user.id", "model.default")
showConvert(userId, defaultTheme) {
return userId > 0 && !defaultTheme;
},
}
actions: {
updateToLatest() {
this.set("updatingRemote", true);
this.model
.updateToLatest()
.catch(popupAjaxError)
.finally(() => {
this.set("updatingRemote", false);
});
},
checkForThemeUpdates() {
this.set("updatingRemote", true);
this.model
.checkForUpdates()
.catch(popupAjaxError)
.finally(() => {
this.set("updatingRemote", false);
});
},
addUploadModal() {
showModal("admin-add-upload", { admin: true, name: "" });
},
addUpload(info) {
let model = this.model;
model.setField("common", info.name, "", info.upload_id, THEME_UPLOAD_VAR);
model.saveChanges("theme_fields").catch((e) => popupAjaxError(e));
},
cancelChangeScheme() {
this.set("colorSchemeId", this.get("model.color_scheme_id"));
},
changeScheme() {
let schemeId = this.colorSchemeId;
this.set(
"model.color_scheme_id",
schemeId === null ? null : parseInt(schemeId, 10)
);
this.model.saveChanges("color_scheme_id");
},
startEditingName() {
this.set("oldName", this.get("model.name"));
this.set("editingName", true);
},
cancelEditingName() {
this.set("model.name", this.oldName);
this.set("editingName", false);
},
finishedEditingName() {
this.model.saveChanges("name");
this.set("editingName", false);
},
editTheme() {
if (this.get("model.remote_theme.is_git")) {
this.dialog.confirm({
message: I18n.t("admin.customize.theme.edit_confirm"),
didConfirm: () => this.transitionToEditRoute(),
});
} else {
this.transitionToEditRoute();
}
},
applyDefault() {
const model = this.model;
model.saveChanges("default").then(() => {
if (model.get("default")) {
this.allThemes.forEach((theme) => {
if (theme !== model && theme.get("default")) {
theme.set("default", false);
}
});
}
@action
updateToLatest() {
this.set("updatingRemote", true);
this.model
.updateToLatest()
.catch(popupAjaxError)
.finally(() => {
this.set("updatingRemote", false);
});
},
}
applyUserSelectable() {
this.model.saveChanges("user_selectable");
},
applyAutoUpdateable() {
this.model.saveChanges("auto_update");
},
addChildTheme() {
let themeId = parseInt(this.selectedChildThemeId, 10);
let theme = this.allThemes.findBy("id", themeId);
this.model.addChildTheme(theme).then(() => this.store.findAll("theme"));
},
removeUpload(upload) {
return this.dialog.yesNoConfirm({
message: I18n.t("admin.customize.theme.delete_upload_confirm"),
didConfirm: () => this.model.removeField(upload),
@action
checkForThemeUpdates() {
this.set("updatingRemote", true);
this.model
.checkForUpdates()
.catch(popupAjaxError)
.finally(() => {
this.set("updatingRemote", false);
});
},
}
removeChildTheme(theme) {
this.model
.removeChildTheme(theme)
.then(() => this.store.findAll("theme"));
},
@action
addUploadModal() {
showModal("admin-add-upload", { admin: true, name: "" });
}
destroy() {
return this.dialog.yesNoConfirm({
message: I18n.t("admin.customize.delete_confirm", {
theme_name: this.get("model.name"),
}),
didConfirm: () => {
const model = this.model;
model.setProperties({ recentlyInstalled: false });
model.destroyRecord().then(() => {
this.allThemes.removeObject(model);
this.transitionToRoute("adminCustomizeThemes");
});
},
@action
addUpload(info) {
let model = this.model;
model.setField("common", info.name, "", info.upload_id, THEME_UPLOAD_VAR);
model.saveChanges("theme_fields").catch((e) => popupAjaxError(e));
}
@action
cancelChangeScheme() {
this.set("colorSchemeId", this.get("model.color_scheme_id"));
}
@action
changeScheme() {
let schemeId = this.colorSchemeId;
this.set(
"model.color_scheme_id",
schemeId === null ? null : parseInt(schemeId, 10)
);
this.model.saveChanges("color_scheme_id");
}
@action
startEditingName() {
this.set("oldName", this.get("model.name"));
this.set("editingName", true);
}
@action
cancelEditingName() {
this.set("model.name", this.oldName);
this.set("editingName", false);
}
@action
finishedEditingName() {
this.model.saveChanges("name");
this.set("editingName", false);
}
@action
editTheme() {
if (this.get("model.remote_theme.is_git")) {
this.dialog.confirm({
message: I18n.t("admin.customize.theme.edit_confirm"),
didConfirm: () => this.transitionToEditRoute(),
});
},
} else {
this.transitionToEditRoute();
}
}
switchType() {
const relatives = this.get("model.component")
? this.get("model.parentThemes")
: this.get("model.childThemes");
let message = I18n.t(`${this.convertKey}_alert_generic`);
if (relatives && relatives.length > 0) {
message = I18n.t(`${this.convertKey}_alert`, {
relatives: relatives
.map((relative) => relative.get("name"))
.join(", "),
@action
applyDefault() {
const model = this.model;
model.saveChanges("default").then(() => {
if (model.get("default")) {
this.allThemes.forEach((theme) => {
if (theme !== model && theme.get("default")) {
theme.set("default", false);
}
});
}
});
}
return this.dialog.yesNoConfirm({
message,
didConfirm: () => this.commitSwitchType(),
@action
applyUserSelectable() {
this.model.saveChanges("user_selectable");
}
@action
applyAutoUpdateable() {
this.model.saveChanges("auto_update");
}
@action
addChildTheme() {
let themeId = parseInt(this.selectedChildThemeId, 10);
let theme = this.allThemes.findBy("id", themeId);
this.model.addChildTheme(theme).then(() => this.store.findAll("theme"));
}
@action
removeUpload(upload) {
return this.dialog.yesNoConfirm({
message: I18n.t("admin.customize.theme.delete_upload_confirm"),
didConfirm: () => this.model.removeField(upload),
});
}
@action
removeChildTheme(theme) {
this.model.removeChildTheme(theme).then(() => this.store.findAll("theme"));
}
@action
destroyTheme() {
return this.dialog.yesNoConfirm({
message: I18n.t("admin.customize.delete_confirm", {
theme_name: this.get("model.name"),
}),
didConfirm: () => {
const model = this.model;
model.setProperties({ recentlyInstalled: false });
model.destroyRecord().then(() => {
this.allThemes.removeObject(model);
this.transitionToRoute("adminCustomizeThemes");
});
},
});
}
@action
switchType() {
const relatives = this.get("model.component")
? this.get("model.parentThemes")
: this.get("model.childThemes");
let message = I18n.t(`${this.convertKey}_alert_generic`);
if (relatives && relatives.length > 0) {
message = I18n.t(`${this.convertKey}_alert`, {
relatives: relatives.map((relative) => relative.get("name")).join(", "),
});
},
}
enableComponent() {
this.model.set("enabled", true);
this.model
.saveChanges("enabled")
.catch(() => this.model.set("enabled", false));
},
return this.dialog.yesNoConfirm({
message,
didConfirm: () => this.commitSwitchType(),
});
}
disableComponent() {
this.model.set("enabled", false);
this.model
.saveChanges("enabled")
.catch(() => this.model.set("enabled", true));
},
},
});
@action
enableComponent() {
this.model.set("enabled", true);
this.model
.saveChanges("enabled")
.catch(() => this.model.set("enabled", false));
}
@action
disableComponent() {
this.model.set("enabled", false);
this.model
.saveChanges("enabled")
.catch(() => this.model.set("enabled", true));
}
}

View File

@ -2,21 +2,21 @@ import Controller from "@ember/controller";
import { THEMES } from "admin/models/theme";
import discourseComputed from "discourse-common/utils/decorators";
export default Controller.extend({
currentTab: THEMES,
export default class AdminCustomizeThemesController extends Controller {
currentTab = THEMES;
@discourseComputed("model", "model.@each.component")
fullThemes(themes) {
return themes.filter((t) => !t.get("component"));
},
}
@discourseComputed("model", "model.@each.component")
childThemes(themes) {
return themes.filter((t) => t.get("component"));
},
}
@discourseComputed("model.content")
installedThemes(content) {
return content || [];
},
});
}
}

View File

@ -1,9 +1,9 @@
import { computed } from "@ember/object";
import Controller, { inject as controller } from "@ember/controller";
import AdminDashboard from "admin/models/admin-dashboard";
import I18n from "I18n";
import PeriodComputationMixin from "admin/mixins/period-computation";
import Report from "admin/models/report";
import { computed } from "@ember/object";
import discourseComputed from "discourse-common/utils/decorators";
import getURL from "discourse-common/lib/get-url";
import { makeArray } from "discourse-common/lib/helpers";
@ -15,41 +15,49 @@ function staticReport(reportType) {
});
}
export default Controller.extend(PeriodComputationMixin, {
isLoading: false,
dashboardFetchedAt: null,
exceptionController: controller("exception"),
logSearchQueriesEnabled: setting("log_search_queries"),
export default class AdminDashboardGeneralController extends Controller.extend(
PeriodComputationMixin
) {
@controller("exception") exceptionController;
isLoading = false;
dashboardFetchedAt = null;
@setting("log_search_queries") logSearchQueriesEnabled;
@staticReport("users_by_type") usersByTypeReport;
@staticReport("users_by_trust_level") usersByTrustLevelReport;
@staticReport("storage_report") storageReport;
@discourseComputed("siteSettings.dashboard_general_tab_activity_metrics")
activityMetrics(metrics) {
return (metrics || "").split("|").filter(Boolean);
},
}
hiddenReports: computed("siteSettings.dashboard_hidden_reports", function () {
@computed("siteSettings.dashboard_hidden_reports")
get hiddenReports() {
return (this.siteSettings.dashboard_hidden_reports || "")
.split("|")
.filter(Boolean);
}),
}
isActivityMetricsVisible: computed(
"activityMetrics",
"hiddenReports",
function () {
return (
this.activityMetrics.length &&
this.activityMetrics.some((x) => !this.hiddenReports.includes(x))
);
}
),
@computed("activityMetrics", "hiddenReports")
get isActivityMetricsVisible() {
return (
this.activityMetrics.length &&
this.activityMetrics.some((x) => !this.hiddenReports.includes(x))
);
}
isSearchReportsVisible: computed("hiddenReports", function () {
@computed("hiddenReports")
get isSearchReportsVisible() {
return ["top_referred_topics", "trending_search"].some(
(x) => !this.hiddenReports.includes(x)
);
}),
}
isCommunityHealthVisible: computed("hiddenReports", function () {
@computed("hiddenReports")
get isCommunityHealthVisible() {
return [
"consolidated_page_views",
"signups",
@ -59,7 +67,7 @@ export default Controller.extend(PeriodComputationMixin, {
"daily_engaged_users",
"new_contributors",
].some((x) => !this.hiddenReports.includes(x));
}),
}
@discourseComputed
activityMetricsFilters() {
@ -67,14 +75,14 @@ export default Controller.extend(PeriodComputationMixin, {
startDate: this.lastMonth,
endDate: this.today,
};
},
}
@discourseComputed
topReferredTopicsOptions() {
return {
table: { total: false, limit: 8 },
};
},
}
@discourseComputed
topReferredTopicsFilters() {
@ -82,7 +90,7 @@ export default Controller.extend(PeriodComputationMixin, {
startDate: moment().subtract(6, "days").startOf("day"),
endDate: this.today,
};
},
}
@discourseComputed
trendingSearchFilters() {
@ -90,25 +98,21 @@ export default Controller.extend(PeriodComputationMixin, {
startDate: moment().subtract(1, "month").startOf("day"),
endDate: this.today,
};
},
}
@discourseComputed
trendingSearchOptions() {
return {
table: { total: false, limit: 8 },
};
},
}
@discourseComputed
trendingSearchDisabledLabel() {
return I18n.t("admin.dashboard.reports.trending_search.disabled", {
basePath: getURL(""),
});
},
usersByTypeReport: staticReport("users_by_type"),
usersByTrustLevelReport: staticReport("users_by_trust_level"),
storageReport: staticReport("storage_report"),
}
fetchDashboard() {
if (this.isLoading) {
@ -137,14 +141,14 @@ export default Controller.extend(PeriodComputationMixin, {
})
.finally(() => this.set("isLoading", false));
}
},
}
@discourseComputed("startDate", "endDate")
filters(startDate, endDate) {
return { startDate, endDate };
},
}
_reportsForPeriodURL(period) {
return getURL(`/admin?period=${period}`);
},
});
}
}

View File

@ -1,10 +1,12 @@
import { computed } from "@ember/object";
import Controller from "@ember/controller";
import PeriodComputationMixin from "admin/mixins/period-computation";
import { computed } from "@ember/object";
import discourseComputed from "discourse-common/utils/decorators";
import getURL from "discourse-common/lib/get-url";
export default Controller.extend(PeriodComputationMixin, {
export default class AdminDashboardModerationController extends Controller.extend(
PeriodComputationMixin
) {
@discourseComputed
flagsStatusOptions() {
return {
@ -13,17 +15,15 @@ export default Controller.extend(PeriodComputationMixin, {
perPage: 10,
},
};
},
}
isModeratorsActivityVisible: computed(
"siteSettings.dashboard_hidden_reports",
function () {
return !(this.siteSettings.dashboard_hidden_reports || "")
.split("|")
.filter(Boolean)
.includes("moderators_activity");
}
),
@computed("siteSettings.dashboard_hidden_reports")
get isModeratorsActivityVisible() {
return !(this.siteSettings.dashboard_hidden_reports || "")
.split("|")
.filter(Boolean)
.includes("moderators_activity");
}
@discourseComputed
userFlaggingRatioOptions() {
@ -33,19 +33,19 @@ export default Controller.extend(PeriodComputationMixin, {
perPage: 10,
},
};
},
}
@discourseComputed("startDate", "endDate")
filters(startDate, endDate) {
return { startDate, endDate };
},
}
@discourseComputed("lastWeek", "endDate")
lastWeekfilters(startDate, endDate) {
return { startDate, endDate };
},
}
_reportsForPeriodURL(period) {
return getURL(`/admin/dashboard/moderation?period=${period}`);
},
});
}
}

View File

@ -2,10 +2,10 @@ import Controller from "@ember/controller";
import { INPUT_DELAY } from "discourse-common/config/environment";
import discourseComputed from "discourse-common/utils/decorators";
import discourseDebounce from "discourse-common/lib/debounce";
import { get } from "@ember/object";
import { action, get } from "@ember/object";
export default Controller.extend({
filter: null,
export default class AdminDashboardReportsController extends Controller {
filter = null;
@discourseComputed(
"model.[]",
@ -29,15 +29,14 @@ export default Controller.extend({
reports = reports.filter((report) => !hiddenReports.includes(report.type));
return reports;
},
}
actions: {
filterReports(filter) {
discourseDebounce(this, this._performFiltering, filter, INPUT_DELAY);
},
},
@action
filterReports(filter) {
discourseDebounce(this, this._performFiltering, filter, INPUT_DELAY);
}
_performFiltering(filter) {
this.set("filter", filter);
},
});
}
}

View File

@ -1,17 +1,19 @@
import { action, computed } from "@ember/object";
import Controller, { inject as controller } from "@ember/controller";
import AdminDashboard from "admin/models/admin-dashboard";
import VersionCheck from "admin/models/version-check";
import { computed } from "@ember/object";
import discourseComputed from "discourse-common/utils/decorators";
import { setting } from "discourse/lib/computed";
const PROBLEMS_CHECK_MINUTES = 1;
export default Controller.extend({
isLoading: false,
dashboardFetchedAt: null,
exceptionController: controller("exception"),
showVersionChecks: setting("version_checks"),
export default class AdminDashboardController extends Controller {
@controller("exception") exceptionController;
isLoading = false;
dashboardFetchedAt = null;
@setting("version_checks") showVersionChecks;
@discourseComputed(
"lowPriorityProblems.length",
@ -21,25 +23,29 @@ export default Controller.extend({
const problemsLength =
lowPriorityProblemsLength + highPriorityProblemsLength;
return this.currentUser.admin && problemsLength > 0;
},
}
visibleTabs: computed("siteSettings.dashboard_visible_tabs", function () {
@computed("siteSettings.dashboard_visible_tabs")
get visibleTabs() {
return (this.siteSettings.dashboard_visible_tabs || "")
.split("|")
.filter(Boolean);
}),
}
isModerationTabVisible: computed("visibleTabs", function () {
@computed("visibleTabs")
get isModerationTabVisible() {
return this.visibleTabs.includes("moderation");
}),
}
isSecurityTabVisible: computed("visibleTabs", function () {
@computed("visibleTabs")
get isSecurityTabVisible() {
return this.visibleTabs.includes("security");
}),
}
isReportsTabVisible: computed("visibleTabs", function () {
@computed("visibleTabs")
get isReportsTabVisible() {
return this.visibleTabs.includes("reports");
}),
}
fetchProblems() {
if (this.isLoadingProblems) {
@ -53,7 +59,7 @@ export default Controller.extend({
) {
this._loadProblems();
}
},
}
fetchDashboard() {
const versionChecks = this.siteSettings.version_checks;
@ -88,7 +94,7 @@ export default Controller.extend({
this.set("isLoading", false);
});
}
},
}
_loadProblems() {
this.setProperties({
@ -108,16 +114,15 @@ export default Controller.extend({
);
})
.finally(() => this.set("loadingProblems", false));
},
}
@discourseComputed("problemsFetchedAt")
problemsTimestamp(problemsFetchedAt) {
return moment(problemsFetchedAt).locale("en").format("LLL");
},
}
actions: {
refreshProblems() {
this._loadProblems();
},
},
});
@action
refreshProblems() {
this._loadProblems();
}
}

View File

@ -1,31 +1,31 @@
import { action } from "@ember/object";
import Controller from "@ember/controller";
import { ajax } from "discourse/lib/ajax";
import { popupAjaxError } from "discourse/lib/ajax-error";
export default Controller.extend({
email: null,
text: null,
elided: null,
format: null,
loading: null,
export default class AdminEmailAdvancedTestController extends Controller {
email = null;
text = null;
elided = null;
format = null;
loading = null;
actions: {
run() {
this.set("loading", true);
@action
run() {
this.set("loading", true);
ajax("/admin/email/advanced-test", {
type: "POST",
data: { email: this.email },
ajax("/admin/email/advanced-test", {
type: "POST",
data: { email: this.email },
})
.then((data) => {
this.setProperties({
text: data.text,
elided: data.elided,
format: data.format,
});
})
.then((data) => {
this.setProperties({
text: data.text,
elided: data.elided,
format: data.format,
});
})
.catch(popupAjaxError)
.finally(() => this.set("loading", false));
},
},
});
.catch(popupAjaxError)
.finally(() => this.set("loading", false));
}
}

View File

@ -1,18 +1,18 @@
import AdminEmailLogsController from "admin/controllers/admin-email-logs";
import { INPUT_DELAY } from "discourse-common/config/environment";
import discourseDebounce from "discourse-common/lib/debounce";
import { observes } from "discourse-common/utils/decorators";
import { observes } from "@ember-decorators/object";
import { action } from "@ember/object";
export default AdminEmailLogsController.extend({
export default class AdminEmailBouncedController extends AdminEmailLogsController {
@action
handleShowIncomingEmail(id, event) {
event?.preventDefault();
this.send("showIncomingEmail", id);
},
}
@observes("filter.{status,user,address,type}")
filterEmailLogs() {
discourseDebounce(this, this.loadLogs, INPUT_DELAY);
},
});
}
}

View File

@ -1,21 +1,22 @@
import { action } from "@ember/object";
import { inject as service } from "@ember/service";
import { empty } from "@ember/object/computed";
import Controller from "@ember/controller";
import I18n from "I18n";
import { ajax } from "discourse/lib/ajax";
import { empty } from "@ember/object/computed";
import { observes } from "discourse-common/utils/decorators";
import { inject as service } from "@ember/service";
import { observes } from "@ember-decorators/object";
import { htmlSafe } from "@ember/template";
import { escapeExpression } from "discourse/lib/utilities";
export default Controller.extend({
dialog: service(),
export default class AdminEmailIndexController extends Controller {
@service dialog;
/**
Is the "send test email" button disabled?
@property sendTestEmailDisabled
**/
sendTestEmailDisabled: empty("testEmailAddress"),
@empty("testEmailAddress") sendTestEmailDisabled;
/**
Clears the 'sentTestEmail' property on successful send.
@ -25,43 +26,40 @@ export default Controller.extend({
@observes("testEmailAddress")
testEmailAddressChanged() {
this.set("sentTestEmail", false);
},
}
actions: {
/**
Sends a test email to the currently entered email address
/**
Sends a test email to the currently entered email address
@method sendTestEmail
**/
sendTestEmail() {
this.setProperties({
sendingEmail: true,
sentTestEmail: false,
});
@method sendTestEmail
**/
@action
sendTestEmail() {
this.setProperties({
sendingEmail: true,
sentTestEmail: false,
});
ajax("/admin/email/test", {
type: "POST",
data: { email_address: this.testEmailAddress },
ajax("/admin/email/test", {
type: "POST",
data: { email_address: this.testEmailAddress },
})
.then((response) =>
this.set("sentTestEmailMessage", response.sent_test_email_message)
)
.catch((e) => {
if (e.jqXHR.responseJSON?.errors) {
this.dialog.alert({
message: htmlSafe(
I18n.t("admin.email.error", {
server_error: escapeExpression(e.jqXHR.responseJSON.errors[0]),
})
),
});
} else {
this.dialog.alert({ message: I18n.t("admin.email.test_error") });
}
})
.then((response) =>
this.set("sentTestEmailMessage", response.sent_test_email_message)
)
.catch((e) => {
if (e.jqXHR.responseJSON?.errors) {
this.dialog.alert({
message: htmlSafe(
I18n.t("admin.email.error", {
server_error: escapeExpression(
e.jqXHR.responseJSON.errors[0]
),
})
),
});
} else {
this.dialog.alert({ message: I18n.t("admin.email.test_error") });
}
})
.finally(() => this.set("sendingEmail", false));
},
},
});
.finally(() => this.set("sendingEmail", false));
}
}

View File

@ -1,14 +1,11 @@
import Controller from "@ember/controller";
import EmailLog from "admin/models/email-log";
import EmberObject from "@ember/object";
import EmberObject, { action } from "@ember/object";
export default Controller.extend({
loading: false,
export default class AdminEmailLogsController extends Controller {
loading = false;
filter = EmberObject.create();
init() {
this._super(...arguments);
this.set("filter", EmberObject.create());
},
loadLogs(sourceModel, loadMore) {
if ((loadMore && this.loading) || this.get("model.allLoaded")) {
return;
@ -38,11 +35,10 @@ export default Controller.extend({
}
})
.finally(() => this.set("loading", false));
},
}
actions: {
loadMore() {
this.loadLogs(EmailLog, true);
},
},
});
@action
loadMore() {
this.loadLogs(EmailLog, true);
}
}

View File

@ -1,66 +1,67 @@
import { inject as service } from "@ember/service";
import { empty, notEmpty, or } from "@ember/object/computed";
import Controller from "@ember/controller";
import EmailPreview from "admin/models/email-preview";
import { action, get } from "@ember/object";
import { popupAjaxError } from "discourse/lib/ajax-error";
import { inject as service } from "@ember/service";
export default Controller.extend({
dialog: service(),
username: null,
lastSeen: null,
emailEmpty: empty("email"),
sendEmailDisabled: or("emailEmpty", "sendingEmail"),
showSendEmailForm: notEmpty("model.html_content"),
htmlEmpty: empty("model.html_content"),
export default class AdminEmailPreviewDigestController extends Controller {
@service dialog;
username = null;
lastSeen = null;
@empty("email") emailEmpty;
@or("emailEmpty", "sendingEmail") sendEmailDisabled;
@notEmpty("model.html_content") showSendEmailForm;
@empty("model.html_content") htmlEmpty;
@action
toggleShowHtml(event) {
event?.preventDefault();
this.toggleProperty("showHtml");
},
}
actions: {
updateUsername(selected) {
this.set("username", get(selected, "firstObject"));
},
@action
updateUsername(selected) {
this.set("username", get(selected, "firstObject"));
}
refresh() {
const model = this.model;
@action
refresh() {
const model = this.model;
this.set("loading", true);
this.set("sentEmail", false);
this.set("loading", true);
this.set("sentEmail", false);
let username = this.username;
if (!username) {
username = this.currentUser.get("username");
this.set("username", username);
}
let username = this.username;
if (!username) {
username = this.currentUser.get("username");
this.set("username", username);
}
EmailPreview.findDigest(username, this.lastSeen).then((email) => {
model.setProperties(
email.getProperties("html_content", "text_content")
);
this.set("loading", false);
EmailPreview.findDigest(username, this.lastSeen).then((email) => {
model.setProperties(email.getProperties("html_content", "text_content"));
this.set("loading", false);
});
}
@action
sendEmail() {
this.set("sendingEmail", true);
this.set("sentEmail", false);
EmailPreview.sendDigest(this.username, this.lastSeen, this.email)
.then((result) => {
if (result.errors) {
this.dialog.alert(result.errors);
} else {
this.set("sentEmail", true);
}
})
.catch(popupAjaxError)
.finally(() => {
this.set("sendingEmail", false);
});
},
sendEmail() {
this.set("sendingEmail", true);
this.set("sentEmail", false);
EmailPreview.sendDigest(this.username, this.lastSeen, this.email)
.then((result) => {
if (result.errors) {
this.dialog.alert(result.errors);
} else {
this.set("sentEmail", true);
}
})
.catch(popupAjaxError)
.finally(() => {
this.set("sendingEmail", false);
});
},
},
});
}
}

View File

@ -1,18 +1,18 @@
import { action } from "@ember/object";
import AdminEmailLogsController from "admin/controllers/admin-email-logs";
import { INPUT_DELAY } from "discourse-common/config/environment";
import IncomingEmail from "admin/models/incoming-email";
import discourseDebounce from "discourse-common/lib/debounce";
import { observes } from "discourse-common/utils/decorators";
import { observes } from "@ember-decorators/object";
export default AdminEmailLogsController.extend({
export default class AdminEmailReceivedController extends AdminEmailLogsController {
@observes("filter.{status,from,to,subject}")
filterIncomingEmails() {
discourseDebounce(this, this.loadLogs, IncomingEmail, INPUT_DELAY);
},
}
actions: {
loadMore() {
this.loadLogs(IncomingEmail, true);
},
},
});
@action
loadMore() {
this.loadLogs(IncomingEmail, true);
}
}

View File

@ -2,24 +2,23 @@ import AdminEmailLogsController from "admin/controllers/admin-email-logs";
import { INPUT_DELAY } from "discourse-common/config/environment";
import IncomingEmail from "admin/models/incoming-email";
import discourseDebounce from "discourse-common/lib/debounce";
import { observes } from "discourse-common/utils/decorators";
import { observes } from "@ember-decorators/object";
import { action } from "@ember/object";
export default AdminEmailLogsController.extend({
export default class AdminEmailRejectedController extends AdminEmailLogsController {
@observes("filter.{status,from,to,subject,error}")
filterIncomingEmails() {
discourseDebounce(this, this.loadLogs, IncomingEmail, INPUT_DELAY);
},
}
@action
handleShowIncomingEmail(id, event) {
event?.preventDefault();
this.send("showIncomingEmail", id);
},
}
actions: {
loadMore() {
this.loadLogs(IncomingEmail, true);
},
},
});
@action
loadMore() {
this.loadLogs(IncomingEmail, true);
}
}

View File

@ -1,11 +1,11 @@
import AdminEmailLogsController from "admin/controllers/admin-email-logs";
import { INPUT_DELAY } from "discourse-common/config/environment";
import discourseDebounce from "discourse-common/lib/debounce";
import { observes } from "discourse-common/utils/decorators";
import { observes } from "@ember-decorators/object";
export default AdminEmailLogsController.extend({
export default class AdminEmailSentController extends AdminEmailLogsController {
@observes("filter.{status,user,address,type,reply_key}")
filterEmailLogs() {
discourseDebounce(this, this.loadLogs, INPUT_DELAY);
},
});
}
}

View File

@ -1,11 +1,11 @@
import AdminEmailLogsController from "admin/controllers/admin-email-logs";
import { INPUT_DELAY } from "discourse-common/config/environment";
import discourseDebounce from "discourse-common/lib/debounce";
import { observes } from "discourse-common/utils/decorators";
import { observes } from "@ember-decorators/object";
export default AdminEmailLogsController.extend({
export default class AdminEmailSkippedController extends AdminEmailLogsController {
@observes("filter.{status,user,address,type}")
filterEmailLogs() {
discourseDebounce(this, this.loadLogs, INPUT_DELAY);
},
});
}
}

View File

@ -1,17 +1,18 @@
import { action } from "@ember/object";
import Controller from "@ember/controller";
import discourseComputed from "discourse-common/utils/decorators";
import { popupAjaxError } from "discourse/lib/ajax-error";
export default Controller.extend({
saved: false,
embedding: null,
export default class AdminEmbeddingController extends Controller {
saved = false;
embedding = null;
// show settings if we have at least one created host
@discourseComputed("embedding.embeddable_hosts.@each.isCreated")
showSecondary() {
const hosts = this.get("embedding.embeddable_hosts");
return hosts.length && hosts.findBy("isCreated");
},
}
@discourseComputed("embedding.base_url")
embeddingCode(baseUrl) {
@ -33,27 +34,28 @@ export default Controller.extend({
</script>`;
return html;
},
}
actions: {
saveChanges() {
const embedding = this.embedding;
const updates = embedding.getProperties(embedding.get("fields"));
@action
saveChanges() {
const embedding = this.embedding;
const updates = embedding.getProperties(embedding.get("fields"));
this.set("saved", false);
this.embedding
.update(updates)
.then(() => this.set("saved", true))
.catch(popupAjaxError);
},
this.set("saved", false);
this.embedding
.update(updates)
.then(() => this.set("saved", true))
.catch(popupAjaxError);
}
addHost() {
const host = this.store.createRecord("embeddable-host");
this.get("embedding.embeddable_hosts").pushObject(host);
},
@action
addHost() {
const host = this.store.createRecord("embeddable-host");
this.get("embedding.embeddable_hosts").pushObject(host);
}
deleteHost(host) {
this.get("embedding.embeddable_hosts").removeObject(host);
},
},
});
@action
deleteHost(host) {
this.get("embedding.embeddable_hosts").removeObject(host);
}
}

View File

@ -1,49 +1,46 @@
import { inject as service } from "@ember/service";
import { sort } from "@ember/object/computed";
import EmberObject, { action, computed } from "@ember/object";
import Controller from "@ember/controller";
import I18n from "I18n";
import { ajax } from "discourse/lib/ajax";
import { sort } from "@ember/object/computed";
import { inject as service } from "@ember/service";
const ALL_FILTER = "all";
export default Controller.extend({
dialog: service(),
filter: null,
sorting: null,
export default class AdminEmojisController extends Controller {
@service dialog;
filter = null;
sorting = null;
@sort("filteredEmojis.[]", "sorting") sortedEmojis;
init() {
this._super(...arguments);
super.init(...arguments);
this.setProperties({
filter: ALL_FILTER,
sorting: ["group", "name"],
});
},
}
sortedEmojis: sort("filteredEmojis.[]", "sorting"),
@computed("model")
get emojiGroups() {
return this.model.mapBy("group").uniq();
}
emojiGroups: computed("model", {
get() {
return this.model.mapBy("group").uniq();
},
}),
@computed("emojiGroups.[]")
get sortingGroups() {
return [ALL_FILTER].concat(this.emojiGroups);
}
sortingGroups: computed("emojiGroups.[]", {
get() {
return [ALL_FILTER].concat(this.emojiGroups);
},
}),
filteredEmojis: computed("model.[]", "filter", {
get() {
if (!this.filter || this.filter === ALL_FILTER) {
return this.model;
} else {
return this.model.filterBy("group", this.filter);
}
},
}),
@computed("model.[]", "filter")
get filteredEmojis() {
if (!this.filter || this.filter === ALL_FILTER) {
return this.model;
} else {
return this.model.filterBy("group", this.filter);
}
}
_highlightEmojiList() {
const customEmojiListEl = document.querySelector("#custom_emoji");
@ -56,12 +53,12 @@ export default Controller.extend({
customEmojiListEl.classList.remove("highlighted");
});
}
},
}
@action
filterGroups(value) {
this.set("filter", value);
},
}
@action
emojiUploaded(emoji, group) {
@ -69,7 +66,7 @@ export default Controller.extend({
emoji.group = group;
this.model.pushObject(EmberObject.create(emoji));
this._highlightEmojiList();
},
}
@action
destroyEmoji(emoji) {
@ -85,5 +82,5 @@ export default Controller.extend({
});
},
});
},
});
}
}

View File

@ -1,23 +1,24 @@
import { action } from "@ember/object";
import Controller from "@ember/controller";
import ScreenedEmail from "admin/models/screened-email";
import { exportEntity } from "discourse/lib/export-csv";
import { outputExportResult } from "discourse/lib/export-result";
export default Controller.extend({
loading: false,
export default class AdminLogsScreenedEmailsController extends Controller {
loading = false;
actions: {
clearBlock(row) {
row.clearBlock().then(function () {
// feeling lazy
window.location.reload();
});
},
@action
clearBlock(row) {
row.clearBlock().then(function () {
// feeling lazy
window.location.reload();
});
}
exportScreenedEmailList() {
exportEntity("screened_email").then(outputExportResult);
},
},
@action
exportScreenedEmailList() {
exportEntity("screened_email").then(outputExportResult);
}
show() {
this.set("loading", true);
@ -25,5 +26,5 @@ export default Controller.extend({
this.set("model", result);
this.set("loading", false);
});
},
});
}
}

View File

@ -1,31 +1,32 @@
import { inject as service } from "@ember/service";
import Controller from "@ember/controller";
import I18n from "I18n";
import { INPUT_DELAY } from "discourse-common/config/environment";
import ScreenedIpAddress from "admin/models/screened-ip-address";
import discourseDebounce from "discourse-common/lib/debounce";
import { exportEntity } from "discourse/lib/export-csv";
import { observes } from "discourse-common/utils/decorators";
import { observes } from "@ember-decorators/object";
import { outputExportResult } from "discourse/lib/export-result";
import { action } from "@ember/object";
import { inject as service } from "@ember/service";
export default Controller.extend({
dialog: service(),
loading: false,
filter: null,
savedIpAddress: null,
export default class AdminLogsScreenedIpAddressesController extends Controller {
@service dialog;
loading = false;
filter = null;
savedIpAddress = null;
_debouncedShow() {
this.set("loading", true);
ScreenedIpAddress.findAll(this.filter).then((result) => {
this.setProperties({ model: result, loading: false });
});
},
}
@observes("filter")
show() {
discourseDebounce(this, this._debouncedShow, INPUT_DELAY);
},
}
@action
edit(record, event) {
@ -34,81 +35,86 @@ export default Controller.extend({
this.set("savedIpAddress", record.get("ip_address"));
}
record.set("editing", true);
},
}
actions: {
allow(record) {
record.set("action_name", "do_nothing");
record.save();
},
@action
allow(record) {
record.set("action_name", "do_nothing");
record.save();
}
block(record) {
record.set("action_name", "block");
record.save();
},
@action
block(record) {
record.set("action_name", "block");
record.save();
}
cancel(record) {
const savedIpAddress = this.savedIpAddress;
if (savedIpAddress && record.get("editing")) {
record.set("ip_address", savedIpAddress);
}
record.set("editing", false);
},
@action
cancel(record) {
const savedIpAddress = this.savedIpAddress;
if (savedIpAddress && record.get("editing")) {
record.set("ip_address", savedIpAddress);
}
record.set("editing", false);
}
save(record) {
const wasEditing = record.get("editing");
record.set("editing", false);
record
.save()
.then(() => this.set("savedIpAddress", null))
.catch((e) => {
if (e.jqXHR.responseJSON && e.jqXHR.responseJSON.errors) {
@action
save(record) {
const wasEditing = record.get("editing");
record.set("editing", false);
record
.save()
.then(() => this.set("savedIpAddress", null))
.catch((e) => {
if (e.jqXHR.responseJSON && e.jqXHR.responseJSON.errors) {
this.dialog.alert(
I18n.t("generic_error_with_reason", {
error: e.jqXHR.responseJSON.errors.join(". "),
})
);
} else {
this.dialog.alert(I18n.t("generic_error"));
}
if (wasEditing) {
record.set("editing", true);
}
});
}
@action
destroyRecord(record) {
return this.dialog.yesNoConfirm({
message: I18n.t("admin.logs.screened_ips.delete_confirm", {
ip_address: record.get("ip_address"),
}),
didConfirm: () => {
return record
.destroy()
.then((deleted) => {
if (deleted) {
this.model.removeObject(record);
} else {
this.dialog.alert(I18n.t("generic_error"));
}
})
.catch((e) => {
this.dialog.alert(
I18n.t("generic_error_with_reason", {
error: e.jqXHR.responseJSON.errors.join(". "),
error: `http: ${e.status} - ${e.body}`,
})
);
} else {
this.dialog.alert(I18n.t("generic_error"));
}
if (wasEditing) {
record.set("editing", true);
}
});
},
});
},
});
}
destroy(record) {
return this.dialog.yesNoConfirm({
message: I18n.t("admin.logs.screened_ips.delete_confirm", {
ip_address: record.get("ip_address"),
}),
didConfirm: () => {
return record
.destroy()
.then((deleted) => {
if (deleted) {
this.model.removeObject(record);
} else {
this.dialog.alert(I18n.t("generic_error"));
}
})
.catch((e) => {
this.dialog.alert(
I18n.t("generic_error_with_reason", {
error: `http: ${e.status} - ${e.body}`,
})
);
});
},
});
},
@action
recordAdded(arg) {
this.model.unshiftObject(arg);
}
recordAdded(arg) {
this.model.unshiftObject(arg);
},
exportScreenedIpList() {
exportEntity("screened_ip").then(outputExportResult);
},
},
});
@action
exportScreenedIpList() {
exportEntity("screened_ip").then(outputExportResult);
}
}

View File

@ -1,10 +1,11 @@
import { action } from "@ember/object";
import Controller from "@ember/controller";
import ScreenedUrl from "admin/models/screened-url";
import { exportEntity } from "discourse/lib/export-csv";
import { outputExportResult } from "discourse/lib/export-result";
export default Controller.extend({
loading: false,
export default class AdminLogsScreenedUrlsController extends Controller {
loading = false;
show() {
this.set("loading", true);
@ -12,11 +13,10 @@ export default Controller.extend({
this.set("model", result);
this.set("loading", false);
});
},
}
actions: {
exportScreenedUrlList() {
exportEntity("screened_url").then(outputExportResult);
},
},
});
@action
exportScreenedUrlList() {
exportEntity("screened_url").then(outputExportResult);
}
}

View File

@ -7,22 +7,21 @@ import { outputExportResult } from "discourse/lib/export-result";
import { scheduleOnce } from "@ember/runloop";
import showModal from "discourse/lib/show-modal";
export default Controller.extend({
queryParams: ["filters"],
model: null,
filters: null,
userHistoryActions: null,
export default class AdminLogsStaffActionLogsController extends Controller {
queryParams = ["filters"];
model = null;
filters = null;
userHistoryActions = null;
@discourseComputed("filters.action_name")
actionFilter(name) {
return name ? I18n.t("admin.logs.staff_actions.actions." + name) : null;
},
}
@discourseComputed("filters")
filtersExists(filters) {
return filters && Object.keys(filters).length > 0;
},
}
_refresh() {
this.store.findAll("staff-action-log", this.filters).then((result) => {
@ -44,11 +43,11 @@ export default Controller.extend({
);
}
});
},
}
scheduleRefresh() {
scheduleOnce("afterRender", this, this._refresh);
},
}
resetFilters() {
this.setProperties({
@ -56,7 +55,7 @@ export default Controller.extend({
filters: EmberObject.create(),
});
this.scheduleRefresh();
},
}
changeFilters(props) {
this.set("model", EmberObject.create({ loadingMore: true }));
@ -76,7 +75,7 @@ export default Controller.extend({
this.send("onFiltersChange", this.filters);
this.scheduleRefresh();
},
}
@action
filterActionIdChanged(filterActionId) {
@ -87,7 +86,7 @@ export default Controller.extend({
.action_id,
});
}
},
}
@action
clearFilter(key, event) {
@ -102,14 +101,14 @@ export default Controller.extend({
} else {
this.changeFilters({ [key]: null });
}
},
}
@action
clearAllFilters(event) {
event?.preventDefault();
this.set("filterActionId", null);
this.resetFilters();
},
}
@action
filterByAction(logItem, event) {
@ -119,35 +118,35 @@ export default Controller.extend({
action_id: logItem.get("action"),
custom_type: logItem.get("custom_type"),
});
},
}
@action
filterByStaffUser(acting_user, event) {
event?.preventDefault();
this.changeFilters({ acting_user: acting_user.username });
},
}
@action
filterByTargetUser(target_user, event) {
event?.preventDefault();
this.changeFilters({ target_user: target_user.username });
},
}
@action
filterBySubject(subject, event) {
event?.preventDefault();
this.changeFilters({ subject });
},
}
@action
exportStaffActionLogs() {
exportEntity("staff_action").then(outputExportResult);
},
}
@action
loadMore() {
this.model.loadMore();
},
}
@action
showDetailsModal(model, event) {
@ -157,7 +156,7 @@ export default Controller.extend({
admin: true,
modalClass: "log-details-modal",
});
},
}
@action
showCustomDetailsModal(model, event) {
@ -168,5 +167,5 @@ export default Controller.extend({
modalClass: "history-modal",
});
modal.loadDiff();
},
});
}
}

View File

@ -1,59 +1,63 @@
import { action } from "@ember/object";
import { inject as service } from "@ember/service";
import { or } from "@ember/object/computed";
import Controller from "@ember/controller";
import I18n from "I18n";
import { INPUT_DELAY } from "discourse-common/config/environment";
import Permalink from "admin/models/permalink";
import discourseDebounce from "discourse-common/lib/debounce";
import { observes } from "discourse-common/utils/decorators";
import { observes } from "@ember-decorators/object";
import { clipboardCopy } from "discourse/lib/utilities";
import { inject as service } from "@ember/service";
import { or } from "@ember/object/computed";
export default Controller.extend({
dialog: service(),
loading: false,
filter: null,
showSearch: or("model.length", "filter"),
export default class AdminPermalinksController extends Controller {
@service dialog;
loading = false;
filter = null;
@or("model.length", "filter") showSearch;
_debouncedShow() {
Permalink.findAll(this.filter).then((result) => {
this.set("model", result);
this.set("loading", false);
});
},
}
@observes("filter")
show() {
discourseDebounce(this, this._debouncedShow, INPUT_DELAY);
},
}
actions: {
recordAdded(arg) {
this.model.unshiftObject(arg);
},
@action
recordAdded(arg) {
this.model.unshiftObject(arg);
}
copyUrl(pl) {
let linkElement = document.querySelector(`#admin-permalink-${pl.id}`);
clipboardCopy(linkElement.textContent);
},
@action
copyUrl(pl) {
let linkElement = document.querySelector(`#admin-permalink-${pl.id}`);
clipboardCopy(linkElement.textContent);
}
destroy(record) {
return this.dialog.yesNoConfirm({
message: I18n.t("admin.permalink.delete_confirm"),
didConfirm: () => {
return record.destroy().then(
(deleted) => {
if (deleted) {
this.model.removeObject(record);
} else {
this.dialog.alert(I18n.t("generic_error"));
}
},
function () {
@action
destroyRecord(record) {
return this.dialog.yesNoConfirm({
message: I18n.t("admin.permalink.delete_confirm"),
didConfirm: () => {
return record.destroy().then(
(deleted) => {
if (deleted) {
this.model.removeObject(record);
} else {
this.dialog.alert(I18n.t("generic_error"));
}
);
},
});
},
},
});
},
function () {
this.dialog.alert(I18n.t("generic_error"));
}
);
},
});
}
}

View File

@ -1,18 +1,18 @@
import Controller from "@ember/controller";
import { inject as service } from "@ember/service";
import Controller from "@ember/controller";
export default Controller.extend({
router: service(),
export default class AdminPluginsController extends Controller {
@service router;
get adminRoutes() {
return this.allAdminRoutes.filter((r) => this.routeExists(r.full_location));
},
}
get brokenAdminRoutes() {
return this.allAdminRoutes.filter(
(r) => !this.routeExists(r.full_location)
);
},
}
get allAdminRoutes() {
return this.model
@ -21,7 +21,7 @@ export default Controller.extend({
return p.admin_route;
})
.filter(Boolean);
},
}
routeExists(routeName) {
try {
@ -30,5 +30,5 @@ export default Controller.extend({
} catch (e) {
return false;
}
},
});
}
}

View File

@ -1,12 +1,12 @@
import Controller from "@ember/controller";
import discourseComputed from "discourse-common/utils/decorators";
export default Controller.extend({
queryParams: ["start_date", "end_date", "filters", "chart_grouping", "mode"],
start_date: null,
end_date: null,
filters: null,
chart_grouping: null,
export default class AdminReportsShowController extends Controller {
queryParams = ["start_date", "end_date", "filters", "chart_grouping", "mode"];
start_date = null;
end_date = null;
filters = null;
chart_grouping = null;
@discourseComputed("model.type")
reportOptions(type) {
@ -19,5 +19,5 @@ export default Controller.extend({
options.chartGrouping = this.chart_grouping;
return options;
},
});
}
}

View File

@ -2,24 +2,19 @@ import Controller from "@ember/controller";
import I18n from "I18n";
export const DEFAULT_PERIOD = "yearly";
export default Controller.extend({
loading: false,
period: DEFAULT_PERIOD,
searchType: "all",
init() {
this._super(...arguments);
this.searchTypeOptions = [
{
id: "all",
name: I18n.t("admin.logs.search_logs.types.all_search_types"),
},
{ id: "header", name: I18n.t("admin.logs.search_logs.types.header") },
{
id: "full_page",
name: I18n.t("admin.logs.search_logs.types.full_page"),
},
];
},
});
export default class AdminSearchLogsIndexController extends Controller {
loading = false;
period = DEFAULT_PERIOD;
searchType = "all";
searchTypeOptions = [
{
id: "all",
name: I18n.t("admin.logs.search_logs.types.all_search_types"),
},
{ id: "header", name: I18n.t("admin.logs.search_logs.types.header") },
{
id: "full_page",
name: I18n.t("admin.logs.search_logs.types.full_page"),
},
];
}

View File

@ -2,29 +2,24 @@ import Controller from "@ember/controller";
import { DEFAULT_PERIOD } from "admin/controllers/admin-search-logs-index";
import I18n from "I18n";
export default Controller.extend({
loading: false,
term: null,
period: DEFAULT_PERIOD,
searchType: "all",
init() {
this._super(...arguments);
this.searchTypeOptions = [
{
id: "all",
name: I18n.t("admin.logs.search_logs.types.all_search_types"),
},
{ id: "header", name: I18n.t("admin.logs.search_logs.types.header") },
{
id: "full_page",
name: I18n.t("admin.logs.search_logs.types.full_page"),
},
{
id: "click_through_only",
name: I18n.t("admin.logs.search_logs.types.click_through_only"),
},
];
},
});
export default class AdminSearchLogsTermController extends Controller {
loading = false;
term = null;
period = DEFAULT_PERIOD;
searchType = "all";
searchTypeOptions = [
{
id: "all",
name: I18n.t("admin.logs.search_logs.types.all_search_types"),
},
{ id: "header", name: I18n.t("admin.logs.search_logs.types.header") },
{
id: "full_page",
name: I18n.t("admin.logs.search_logs.types.full_page"),
},
{
id: "click_through_only",
name: I18n.t("admin.logs.search_logs.types.click_through_only"),
},
];
}

View File

@ -1,17 +1,18 @@
import Controller, { inject as controller } from "@ember/controller";
import discourseComputed from "discourse-common/utils/decorators";
export default Controller.extend({
adminSiteSettings: controller(),
categoryNameKey: null,
export default class AdminSiteSettingsCategoryController extends Controller {
@controller adminSiteSettings;
categoryNameKey = null;
@discourseComputed("adminSiteSettings.visibleSiteSettings", "categoryNameKey")
category(categories, nameKey) {
return (categories || []).findBy("nameKey", nameKey);
},
}
@discourseComputed("category")
filteredContent(category) {
return category ? category.siteSettings : [];
},
});
}
}

View File

@ -1,16 +1,19 @@
import { alias } from "@ember/object/computed";
import Controller from "@ember/controller";
import I18n from "I18n";
import { INPUT_DELAY } from "discourse-common/config/environment";
import { alias } from "@ember/object/computed";
import { isEmpty } from "@ember/utils";
import { debounce, observes } from "discourse-common/utils/decorators";
import { debounce } from "discourse-common/utils/decorators";
import { observes } from "@ember-decorators/object";
import { action } from "@ember/object";
export default Controller.extend({
filter: null,
allSiteSettings: alias("model"),
visibleSiteSettings: null,
onlyOverridden: false,
export default class AdminSiteSettingsController extends Controller {
filter = null;
@alias("model") allSiteSettings;
visibleSiteSettings = null;
onlyOverridden = false;
filterContentNow(category) {
// If we have no content, don't bother filtering anything
@ -109,9 +112,13 @@ export default Controller.extend({
"adminSiteSettingsCategory",
category || "all_results"
);
},
}
@observes("filter", "onlyOverridden", "model")
optsChanged() {
this.filterContent();
}
@debounce(INPUT_DELAY)
filterContent() {
if (this._skipBounce) {
@ -119,12 +126,12 @@ export default Controller.extend({
} else {
this.filterContentNow(this.categoryNameKey);
}
},
}
@action
clearFilter() {
this.setProperties({ filter: "", onlyOverridden: false });
},
}
@action
toggleMenu() {
@ -132,5 +139,5 @@ export default Controller.extend({
["mobile-closed", "mobile-open"].forEach((state) => {
adminDetail.classList.toggle(state);
});
},
});
}
}

View File

@ -1,23 +1,23 @@
import { action } from "@ember/object";
import Controller from "@ember/controller";
import discourseComputed from "discourse-common/utils/decorators";
import discourseDebounce from "discourse-common/lib/debounce";
let lastSearch;
export default Controller.extend({
searching: false,
siteTexts: null,
preferred: false,
queryParams: ["q", "overridden", "locale"],
locale: null,
q: null,
overridden: false,
export default class AdminSiteTextIndexController extends Controller {
searching = false;
siteTexts = null;
preferred = false;
queryParams = ["q", "overridden", "locale"];
locale = null;
q = null;
overridden = false;
init() {
this._super(...arguments);
super.init(...arguments);
this.set("locale", this.siteSettings.default_locale);
},
}
_performSearch() {
this.store
@ -26,12 +26,12 @@ export default Controller.extend({
this.set("siteTexts", results);
})
.finally(() => this.set("searching", false));
},
}
@discourseComputed()
availableLocales() {
return JSON.parse(this.siteSettings.available_locales);
},
}
@discourseComputed("locale")
fallbackLocaleFullName() {
@ -40,39 +40,41 @@ export default Controller.extend({
return l.value === this.siteTexts.extras.fallback_locale;
}).name;
}
},
}
actions: {
edit(siteText) {
this.transitionToRoute("adminSiteText.edit", siteText.get("id"), {
queryParams: {
locale: this.locale,
},
});
},
@action
edit(siteText) {
this.transitionToRoute("adminSiteText.edit", siteText.get("id"), {
queryParams: {
locale: this.locale,
},
});
}
toggleOverridden() {
this.toggleProperty("overridden");
@action
toggleOverridden() {
this.toggleProperty("overridden");
this.set("searching", true);
discourseDebounce(this, this._performSearch, 400);
}
@action
search() {
const q = this.q;
if (q !== lastSearch) {
this.set("searching", true);
discourseDebounce(this, this._performSearch, 400);
},
lastSearch = q;
}
}
search() {
const q = this.q;
if (q !== lastSearch) {
this.set("searching", true);
discourseDebounce(this, this._performSearch, 400);
lastSearch = q;
}
},
@action
updateLocale(value) {
this.setProperties({
searching: true,
locale: value,
});
updateLocale(value) {
this.setProperties({
searching: true,
locale: value,
});
discourseDebounce(this, this._performSearch, 400);
},
},
});
discourseDebounce(this, this._performSearch, 400);
}
}

View File

@ -1,25 +1,25 @@
import Controller, { inject as controller } from "@ember/controller";
import { action } from "@ember/object";
import { inject as service } from "@ember/service";
import { alias, sort } from "@ember/object/computed";
import Controller, { inject as controller } from "@ember/controller";
import GrantBadgeController from "discourse/mixins/grant-badge-controller";
import I18n from "I18n";
import discourseComputed from "discourse-common/utils/decorators";
import { next } from "@ember/runloop";
import { popupAjaxError } from "discourse/lib/ajax-error";
import { inject as service } from "@ember/service";
export default Controller.extend(GrantBadgeController, {
adminUser: controller(),
dialog: service(),
user: alias("adminUser.model"),
userBadges: alias("model"),
allBadges: alias("badges"),
sortedBadges: sort("model", "badgeSortOrder"),
export default class AdminUserBadgesController extends Controller.extend(
GrantBadgeController
) {
@service dialog;
@controller adminUser;
init() {
this._super(...arguments);
@alias("adminUser.model") user;
@alias("model") userBadges;
@alias("badges") allBadges;
@sort("model", "badgeSortOrder") sortedBadges;
this.badgeSortOrder = ["granted_at:desc"];
},
badgeSortOrder = ["granted_at:desc"];
@discourseComputed("model", "model.[]", "model.expandedBadges.[]")
groupedBadges() {
@ -59,46 +59,47 @@ export default Controller.extend(GrantBadgeController, {
});
return expanded.sortBy("granted_at").reverse();
},
}
actions: {
expandGroup(userBadge) {
const model = this.model;
model.set("expandedBadges", model.get("expandedBadges") || []);
model.get("expandedBadges").pushObject(userBadge.badge.id);
},
@action
expandGroup(userBadge) {
const model = this.model;
model.set("expandedBadges", model.get("expandedBadges") || []);
model.get("expandedBadges").pushObject(userBadge.badge.id);
}
grantBadge() {
this.grantBadge(
this.selectedBadgeId,
this.get("user.username"),
this.badgeReason
).then(
() => {
this.set("badgeReason", "");
next(() => {
// Update the selected badge ID after the combobox has re-rendered.
const newSelectedBadge = this.grantableBadges[0];
if (newSelectedBadge) {
this.set("selectedBadgeId", newSelectedBadge.get("id"));
}
});
},
function (error) {
popupAjaxError(error);
}
);
},
@action
grantBadge() {
this.grantBadge(
this.selectedBadgeId,
this.get("user.username"),
this.badgeReason
).then(
() => {
this.set("badgeReason", "");
next(() => {
// Update the selected badge ID after the combobox has re-rendered.
const newSelectedBadge = this.grantableBadges[0];
if (newSelectedBadge) {
this.set("selectedBadgeId", newSelectedBadge.get("id"));
}
});
},
function (error) {
popupAjaxError(error);
}
);
}
revokeBadge(userBadge) {
return this.dialog.yesNoConfirm({
message: I18n.t("admin.badges.revoke_confirm"),
didConfirm: () => {
return userBadge.revoke().then(() => {
this.model.removeObject(userBadge);
});
},
});
},
},
});
@action
revokeBadge(userBadge) {
return this.dialog.yesNoConfirm({
message: I18n.t("admin.badges.revoke_confirm"),
didConfirm: () => {
return userBadge.revoke().then(() => {
this.model.removeObject(userBadge);
});
},
});
}
}

View File

@ -1,73 +1,74 @@
import { action } from "@ember/object";
import { inject as service } from "@ember/service";
import { gte, sort } from "@ember/object/computed";
import Controller from "@ember/controller";
import I18n from "I18n";
import { popupAjaxError } from "discourse/lib/ajax-error";
import { inject as service } from "@ember/service";
const MAX_FIELDS = 30;
export default Controller.extend({
dialog: service(),
fieldTypes: null,
createDisabled: gte("model.length", MAX_FIELDS),
sortedFields: sort("model", "fieldSortOrder"),
export default class AdminUserFieldsController extends Controller {
@service dialog;
init() {
this._super(...arguments);
fieldTypes = null;
this.fieldSortOrder = ["position"];
},
@gte("model.length", MAX_FIELDS) createDisabled;
@sort("model", "fieldSortOrder") sortedFields;
actions: {
createField() {
const f = this.store.createRecord("user-field", {
field_type: "text",
position: MAX_FIELDS,
fieldSortOrder = ["position"];
@action
createField() {
const f = this.store.createRecord("user-field", {
field_type: "text",
position: MAX_FIELDS,
});
this.model.pushObject(f);
}
@action
moveUp(f) {
const idx = this.sortedFields.indexOf(f);
if (idx) {
const prev = this.sortedFields.objectAt(idx - 1);
const prevPos = prev.get("position");
prev.update({ position: f.get("position") });
f.update({ position: prevPos });
}
}
@action
moveDown(f) {
const idx = this.sortedFields.indexOf(f);
if (idx > -1) {
const next = this.sortedFields.objectAt(idx + 1);
const nextPos = next.get("position");
next.update({ position: f.get("position") });
f.update({ position: nextPos });
}
}
@action
destroyField(f) {
const model = this.model;
// Only confirm if we already been saved
if (f.get("id")) {
this.dialog.yesNoConfirm({
message: I18n.t("admin.user_fields.delete_confirm"),
didConfirm: () => {
return f
.destroyRecord()
.then(function () {
model.removeObject(f);
})
.catch(popupAjaxError);
},
});
this.model.pushObject(f);
},
moveUp(f) {
const idx = this.sortedFields.indexOf(f);
if (idx) {
const prev = this.sortedFields.objectAt(idx - 1);
const prevPos = prev.get("position");
prev.update({ position: f.get("position") });
f.update({ position: prevPos });
}
},
moveDown(f) {
const idx = this.sortedFields.indexOf(f);
if (idx > -1) {
const next = this.sortedFields.objectAt(idx + 1);
const nextPos = next.get("position");
next.update({ position: f.get("position") });
f.update({ position: nextPos });
}
},
destroy(f) {
const model = this.model;
// Only confirm if we already been saved
if (f.get("id")) {
this.dialog.yesNoConfirm({
message: I18n.t("admin.user_fields.delete_confirm"),
didConfirm: () => {
return f
.destroyRecord()
.then(function () {
model.removeObject(f);
})
.catch(popupAjaxError);
},
});
} else {
model.removeObject(f);
}
},
},
});
} else {
model.removeObject(f);
}
}
}

View File

@ -1,2 +1,2 @@
import Controller from "@ember/controller";
export default Controller.extend();
export default class AdminUserController extends Controller {}

View File

@ -1,4 +1,6 @@
import discourseComputed, { observes } from "discourse-common/utils/decorators";
import { action } from "@ember/object";
import discourseComputed from "discourse-common/utils/decorators";
import { observes } from "@ember-decorators/object";
import AdminUser from "admin/models/admin-user";
import CanCheckEmails from "discourse/mixins/can-check-emails";
import Controller from "@ember/controller";
@ -7,29 +9,28 @@ import { INPUT_DELAY } from "discourse-common/config/environment";
import discourseDebounce from "discourse-common/lib/debounce";
import { i18n } from "discourse/lib/computed";
export default Controller.extend(CanCheckEmails, {
model: null,
query: null,
order: null,
asc: null,
showEmails: false,
refreshing: false,
listFilter: null,
selectAll: false,
searchHint: i18n("search_hint"),
export default class AdminUsersListShowController extends Controller.extend(
CanCheckEmails
) {
model = null;
query = null;
order = null;
asc = null;
showEmails = false;
refreshing = false;
listFilter = null;
selectAll = false;
init() {
this._super(...arguments);
@i18n("search_hint") searchHint;
this._page = 1;
this._results = [];
this._canLoadMore = true;
},
_page = 1;
_results = [];
_canLoadMore = true;
@discourseComputed("query")
title(query) {
return I18n.t("admin.users.titles." + query);
},
}
@discourseComputed("showEmails")
columnCount(showEmails) {
@ -44,19 +45,19 @@ export default Controller.extend(CanCheckEmails, {
}
return colCount;
},
}
@observes("listFilter")
_filterUsers() {
discourseDebounce(this, this.resetFilters, INPUT_DELAY);
},
}
resetFilters() {
this._page = 1;
this._results = [];
this._canLoadMore = true;
this._refreshUsers();
},
}
_refreshUsers() {
if (!this._canLoadMore) {
@ -84,17 +85,17 @@ export default Controller.extend(CanCheckEmails, {
.finally(() => {
this.set("refreshing", false);
});
},
}
actions: {
loadMore() {
this._page += 1;
this._refreshUsers();
},
@action
loadMore() {
this._page += 1;
this._refreshUsers();
}
toggleEmailVisibility() {
this.toggleProperty("showEmails");
this.resetFilters();
},
},
});
@action
toggleEmailVisibility() {
this.toggleProperty("showEmails");
this.resetFilters();
}
}

View File

@ -1,32 +1,35 @@
import { action } from "@ember/object";
import { inject as service } from "@ember/service";
import { or } from "@ember/object/computed";
import Controller, { inject as controller } from "@ember/controller";
import I18n from "I18n";
import WatchedWord from "admin/models/watched-word";
import { ajax } from "discourse/lib/ajax";
import discourseComputed from "discourse-common/utils/decorators";
import { fmt } from "discourse/lib/computed";
import { or } from "@ember/object/computed";
import { schedule } from "@ember/runloop";
import showModal from "discourse/lib/show-modal";
import { inject as service } from "@ember/service";
export default Controller.extend({
adminWatchedWords: controller(),
actionNameKey: null,
dialog: service(),
downloadLink: fmt(
"actionNameKey",
"/admin/customize/watched_words/action/%@/download"
),
showWordsList: or("adminWatchedWords.showWords", "adminWatchedWords.filter"),
export default class AdminWatchedWordsActionController extends Controller {
@service dialog;
@controller adminWatchedWords;
actionNameKey = null;
@fmt("actionNameKey", "/admin/customize/watched_words/action/%@/download")
downloadLink;
@or("adminWatchedWords.showWords", "adminWatchedWords.filter")
showWordsList;
findAction(actionName) {
return (this.adminWatchedWords.model || []).findBy("nameKey", actionName);
},
}
@discourseComputed("actionNameKey", "adminWatchedWords.model")
currentAction(actionName) {
return this.findAction(actionName);
},
}
@discourseComputed("currentAction.words.[]")
regexpError(words) {
@ -37,78 +40,81 @@ export default Controller.extend({
return I18n.t("admin.watched_words.invalid_regex", { word });
}
}
},
}
@discourseComputed("actionNameKey")
actionDescription(actionNameKey) {
return I18n.t("admin.watched_words.action_descriptions." + actionNameKey);
},
}
actions: {
recordAdded(arg) {
const action = this.findAction(this.actionNameKey);
if (!action) {
return;
}
@action
recordAdded(arg) {
const foundAction = this.findAction(this.actionNameKey);
if (!foundAction) {
return;
}
action.words.unshiftObject(arg);
schedule("afterRender", () => {
// remove from other actions lists
let match = null;
this.adminWatchedWords.model.forEach((otherAction) => {
foundAction.words.unshiftObject(arg);
schedule("afterRender", () => {
// remove from other actions lists
let match = null;
this.adminWatchedWords.model.forEach((otherAction) => {
if (match) {
return;
}
if (otherAction.nameKey !== this.actionNameKey) {
match = otherAction.words.findBy("id", arg.id);
if (match) {
return;
otherAction.words.removeObject(match);
}
}
});
});
}
if (otherAction.nameKey !== this.actionNameKey) {
match = otherAction.words.findBy("id", arg.id);
if (match) {
otherAction.words.removeObject(match);
}
@action
recordRemoved(arg) {
if (this.currentAction) {
this.currentAction.words.removeObject(arg);
}
}
@action
uploadComplete() {
WatchedWord.findAll().then((data) => {
this.adminWatchedWords.set("model", data);
});
}
@action
test() {
WatchedWord.findAll().then((data) => {
this.adminWatchedWords.set("model", data);
showModal("admin-watched-word-test", {
admin: true,
model: this.currentAction,
});
});
}
@action
clearAll() {
const actionKey = this.actionNameKey;
this.dialog.yesNoConfirm({
message: I18n.t("admin.watched_words.clear_all_confirm", {
action: I18n.t("admin.watched_words.actions." + actionKey),
}),
didConfirm: () => {
ajax(`/admin/customize/watched_words/action/${actionKey}.json`, {
type: "DELETE",
}).then(() => {
const foundAction = this.findAction(actionKey);
if (foundAction) {
foundAction.set("words", []);
}
});
});
},
recordRemoved(arg) {
if (this.currentAction) {
this.currentAction.words.removeObject(arg);
}
},
uploadComplete() {
WatchedWord.findAll().then((data) => {
this.adminWatchedWords.set("model", data);
});
},
test() {
WatchedWord.findAll().then((data) => {
this.adminWatchedWords.set("model", data);
showModal("admin-watched-word-test", {
admin: true,
model: this.currentAction,
});
});
},
clearAll() {
const actionKey = this.actionNameKey;
this.dialog.yesNoConfirm({
message: I18n.t("admin.watched_words.clear_all_confirm", {
action: I18n.t("admin.watched_words.actions." + actionKey),
}),
didConfirm: () => {
ajax(`/admin/customize/watched_words/action/${actionKey}.json`, {
type: "DELETE",
}).then(() => {
const action = this.findAction(actionKey);
if (action) {
action.set("words", []);
}
});
},
});
},
},
});
},
});
}
}

View File

@ -3,11 +3,11 @@ import EmberObject, { action } from "@ember/object";
import { INPUT_DELAY } from "discourse-common/config/environment";
import discourseDebounce from "discourse-common/lib/debounce";
import { isEmpty } from "@ember/utils";
import { observes } from "discourse-common/utils/decorators";
import { observes } from "@ember-decorators/object";
export default Controller.extend({
filter: null,
showWords: false,
export default class AdminWatchedWordsController extends Controller {
filter = null;
showWords = false;
_filterContent() {
if (isEmpty(this.allWatchedWords)) {
@ -36,17 +36,17 @@ export default Controller.extend({
);
});
this.set("model", model);
},
}
@observes("filter")
filterContent() {
discourseDebounce(this, this._filterContent, INPUT_DELAY);
},
}
@action
clearFilter() {
this.set("filter", "");
},
}
@action
toggleMenu() {
@ -54,5 +54,5 @@ export default Controller.extend({
["mobile-closed", "mobile-open"].forEach((state) => {
adminDetail.classList.toggle(state);
});
},
});
}
}

View File

@ -1,23 +1,24 @@
import { inject as service } from "@ember/service";
import { alias } from "@ember/object/computed";
import Controller, { inject as controller } from "@ember/controller";
import EmberObject, { action } from "@ember/object";
import I18n from "I18n";
import { alias } from "@ember/object/computed";
import discourseComputed from "discourse-common/utils/decorators";
import { isEmpty } from "@ember/utils";
import { popupAjaxError } from "discourse/lib/ajax-error";
import { inject as service } from "@ember/service";
export default Controller.extend({
adminWebHooks: controller(),
dialog: service(),
eventTypes: alias("adminWebHooks.eventTypes"),
defaultEventTypes: alias("adminWebHooks.defaultEventTypes"),
contentTypes: alias("adminWebHooks.contentTypes"),
export default class AdminWebHooksEditController extends Controller {
@service dialog;
@controller adminWebHooks;
@alias("adminWebHooks.eventTypes") eventTypes;
@alias("adminWebHooks.defaultEventTypes") defaultEventTypes;
@alias("adminWebHooks.contentTypes") contentTypes;
@discourseComputed
showTagsFilter() {
return this.siteSettings.tagging_enabled;
},
}
@discourseComputed("model.isSaving", "saved", "saveButtonDisabled")
savingStatus(isSaving, saved, saveButtonDisabled) {
@ -29,14 +30,14 @@ export default Controller.extend({
// Use side effect of validation to clear saved text
this.set("saved", false);
return "";
},
}
@discourseComputed("model.isNew")
saveButtonText(isNew) {
return isNew
? I18n.t("admin.web_hooks.create")
: I18n.t("admin.web_hooks.save");
},
}
@discourseComputed("model.secret")
secretValidation(secret) {
@ -55,7 +56,7 @@ export default Controller.extend({
});
}
}
},
}
@discourseComputed("model.wildcard_web_hook", "model.web_hook_event_types.[]")
eventTypeValidation(isWildcard, eventTypes) {
@ -65,7 +66,7 @@ export default Controller.extend({
reason: I18n.t("admin.web_hooks.event_type_missing"),
});
}
},
}
@discourseComputed(
"model.isSaving",
@ -82,7 +83,7 @@ export default Controller.extend({
return isSaving
? false
: secretValidation || eventTypeValidation || isEmpty(payloadUrl);
},
}
@action
async save() {
@ -97,5 +98,5 @@ export default Controller.extend({
} catch (e) {
popupAjaxError(e);
}
},
});
}
}

View File

@ -1,18 +1,19 @@
import { inject as service } from "@ember/service";
import { alias } from "@ember/object/computed";
import Controller, { inject as controller } from "@ember/controller";
import I18n from "I18n";
import { popupAjaxError } from "discourse/lib/ajax-error";
import { inject as service } from "@ember/service";
import { action } from "@ember/object";
import { alias } from "@ember/object/computed";
export default Controller.extend({
adminWebHooks: controller(),
dialog: service(),
contentTypes: alias("adminWebHooks.contentTypes"),
defaultEventTypes: alias("adminWebHooks.defaultEventTypes"),
deliveryStatuses: alias("adminWebHooks.deliveryStatuses"),
eventTypes: alias("adminWebHooks.eventTypes"),
model: alias("adminWebHooks.model"),
export default class AdminWebHooksIndexController extends Controller {
@service dialog;
@controller adminWebHooks;
@alias("adminWebHooks.contentTypes") contentTypes;
@alias("adminWebHooks.defaultEventTypes") defaultEventTypes;
@alias("adminWebHooks.deliveryStatuses") deliveryStatuses;
@alias("adminWebHooks.eventTypes") eventTypes;
@alias("adminWebHooks.model") model;
@action
destroy(webhook) {
@ -27,10 +28,10 @@ export default Controller.extend({
}
},
});
},
}
@action
loadMore() {
this.model.loadMore();
},
});
}
}

View File

@ -1,18 +1,18 @@
import { inject as service } from "@ember/service";
import Controller, { inject as controller } from "@ember/controller";
import { action } from "@ember/object";
import I18n from "I18n";
import { popupAjaxError } from "discourse/lib/ajax-error";
import { inject as service } from "@ember/service";
export default Controller.extend({
adminWebHooks: controller(),
dialog: service(),
router: service(),
export default class AdminWebHooksShowController extends Controller {
@service dialog;
@service router;
@controller adminWebHooks;
@action
edit() {
return this.router.transitionTo("adminWebHooks.edit", this.model);
},
}
@action
destroy() {
@ -28,5 +28,5 @@ export default Controller.extend({
}
},
});
},
});
}
}

View File

@ -1,3 +1,3 @@
import Controller from "@ember/controller";
export default Controller.extend({});
export default class AdminWebHooksController extends Controller {}

View File

@ -1,20 +1,20 @@
import { inject as service } from "@ember/service";
import Controller from "@ember/controller";
import { dasherize } from "@ember/string";
import discourseComputed from "discourse-common/utils/decorators";
import { inject as service } from "@ember/service";
export default Controller.extend({
router: service(),
export default class AdminController extends Controller {
@service router;
@discourseComputed("siteSettings.enable_group_directory")
showGroups(enableGroupDirectory) {
return !enableGroupDirectory;
},
}
@discourseComputed("siteSettings.enable_badges")
showBadges(enableBadges) {
return this.currentUser.get("admin") && enableBadges;
},
}
@discourseComputed("router._router.currentPath")
adminContentsClassName(currentPath) {
@ -37,5 +37,5 @@ export default Controller.extend({
}
return cssClasses;
},
});
}
}

View File

@ -1,6 +1,8 @@
import Controller, { inject as controller } from "@ember/controller";
import { action } from "@ember/object";
import { and, not } from "@ember/object/computed";
import discourseComputed, { observes } from "discourse-common/utils/decorators";
import Controller, { inject as controller } from "@ember/controller";
import discourseComputed from "discourse-common/utils/decorators";
import { observes } from "@ember-decorators/object";
import I18n from "I18n";
import ModalFunctionality from "discourse/mixins/modal-functionality";
import { ajax } from "discourse/lib/ajax";
@ -53,18 +55,20 @@ const SCSS_VARIABLE_NAMES = [
"love-low",
];
export default Controller.extend(ModalFunctionality, {
adminCustomizeThemesShow: controller(),
export default class AdminAddUploadController extends Controller.extend(
ModalFunctionality
) {
@controller adminCustomizeThemesShow;
uploadUrl: "/admin/themes/upload_asset",
uploadUrl = "/admin/themes/upload_asset";
@and("nameValid", "fileSelected") enabled;
@not("enabled") disabled;
onShow() {
this.set("name", null);
this.set("fileSelected", false);
},
enabled: and("nameValid", "fileSelected"),
disabled: not("enabled"),
}
@discourseComputed("name", "adminCustomizeThemesShow.model.theme_fields")
errorMessage(name, themeFields) {
@ -89,54 +93,54 @@ export default Controller.extend(ModalFunctionality, {
}
return null;
},
}
@discourseComputed("errorMessage")
nameValid(errorMessage) {
return null === errorMessage;
},
}
@observes("name")
uploadChanged() {
const file = $("#file-input")[0];
this.set("fileSelected", file && file.files[0]);
},
}
actions: {
updateName() {
let name = this.name;
if (isEmpty(name)) {
name = $("#file-input")[0].files[0].name;
this.set("name", name.split(".")[0]);
}
this.uploadChanged();
},
@action
updateName() {
let name = this.name;
if (isEmpty(name)) {
name = $("#file-input")[0].files[0].name;
this.set("name", name.split(".")[0]);
}
this.uploadChanged();
}
upload() {
const file = $("#file-input")[0].files[0];
@action
upload() {
const file = $("#file-input")[0].files[0];
const options = {
type: "POST",
processData: false,
contentType: false,
data: new FormData(),
};
const options = {
type: "POST",
processData: false,
contentType: false,
data: new FormData(),
};
options.data.append("file", file);
options.data.append("file", file);
ajax(this.uploadUrl, options)
.then((result) => {
const upload = {
upload_id: result.upload_id,
name: this.name,
original_filename: file.name,
};
this.adminCustomizeThemesShow.send("addUpload", upload);
this.send("closeModal");
})
.catch((e) => {
popupAjaxError(e);
});
},
},
});
ajax(this.uploadUrl, options)
.then((result) => {
const upload = {
upload_id: result.upload_id,
name: this.name,
original_filename: file.name,
};
this.adminCustomizeThemesShow.send("addUpload", upload);
this.send("closeModal");
})
.catch((e) => {
popupAjaxError(e);
});
}
}

View File

@ -4,39 +4,12 @@ import I18n from "I18n";
import discourseComputed from "discourse-common/utils/decorators";
import { escapeExpression } from "discourse/lib/utilities";
export default Controller.extend({
sample: alias("model.sample"),
errors: alias("model.errors"),
count: alias("model.grant_count"),
export default class AdminBadgePreviewController extends Controller {
@alias("model.sample") sample;
@alias("model.errors") errors;
@alias("model.grant_count") count;
@discourseComputed("count", "sample.length")
countWarning(count, sampleLength) {
if (count <= 10) {
return sampleLength !== count;
} else {
return sampleLength !== 10;
}
},
@discourseComputed("model.query_plan")
hasQueryPlan(queryPlan) {
return !!queryPlan;
},
@discourseComputed("model.query_plan")
queryPlanHtml(queryPlan) {
let output = `<pre class="badge-query-plan">`;
queryPlan.forEach((linehash) => {
output += escapeExpression(linehash["QUERY PLAN"]);
output += "<br>";
});
output += "</pre>";
return output;
},
processedSample: map("model.sample", (grant) => {
@map("model.sample", (grant) => {
let i18nKey = "admin.badges.preview.grant.with";
const i18nParams = { username: escapeExpression(grant.username) };
@ -55,5 +28,33 @@ export default Controller.extend({
}
return I18n.t(i18nKey, i18nParams);
}),
});
})
processedSample;
@discourseComputed("count", "sample.length")
countWarning(count, sampleLength) {
if (count <= 10) {
return sampleLength !== count;
} else {
return sampleLength !== 10;
}
}
@discourseComputed("model.query_plan")
hasQueryPlan(queryPlan) {
return !!queryPlan;
}
@discourseComputed("model.query_plan")
queryPlanHtml(queryPlan) {
let output = `<pre class="badge-query-plan">`;
queryPlan.forEach((linehash) => {
output += escapeExpression(linehash["QUERY PLAN"]);
output += "<br>";
});
output += "</pre>";
return output;
}
}

View File

@ -1,27 +1,29 @@
import { action } from "@ember/object";
import Controller, { inject as controller } from "@ember/controller";
import ModalFunctionality from "discourse/mixins/modal-functionality";
export default Controller.extend(ModalFunctionality, {
adminCustomizeColors: controller(),
export default class AdminColorSchemeSelectBaseController extends Controller.extend(
ModalFunctionality
) {
@controller adminCustomizeColors;
selectedBaseThemeId: null,
selectedBaseThemeId = null;
init() {
this._super(...arguments);
super.init(...arguments);
const defaultScheme = this.get(
"adminCustomizeColors.baseColorSchemes.0.base_scheme_id"
);
this.set("selectedBaseThemeId", defaultScheme);
},
}
actions: {
selectBase() {
this.adminCustomizeColors.send(
"newColorSchemeWithBase",
this.selectedBaseThemeId
);
this.send("closeModal");
},
},
});
@action
selectBase() {
this.adminCustomizeColors.send(
"newColorSchemeWithBase",
this.selectedBaseThemeId
);
this.send("closeModal");
}
}

View File

@ -1,18 +1,21 @@
import { alias } from "@ember/object/computed";
import Controller, { inject as controller } from "@ember/controller";
import I18n from "I18n";
import ModalFunctionality from "discourse/mixins/modal-functionality";
import { action } from "@ember/object";
import { alias } from "@ember/object/computed";
import discourseComputed from "discourse-common/utils/decorators";
export default Controller.extend(ModalFunctionality, {
adminUserIndex: controller(),
username: alias("model.username"),
postCount: alias("model.post_count"),
export default class AdminDeletePostsConfirmationController extends Controller.extend(
ModalFunctionality
) {
@controller adminUserIndex;
@alias("model.username") username;
@alias("model.post_count") postCount;
onShow() {
this.set("value", null);
},
}
@discourseComputed("username", "postCount")
text(username, postCount) {
@ -20,27 +23,27 @@ export default Controller.extend(ModalFunctionality, {
username,
postCount,
});
},
}
@discourseComputed("username")
deleteButtonText(username) {
return I18n.t(`admin.user.delete_posts.confirmation.delete`, {
username,
});
},
}
@discourseComputed("value", "text")
deleteDisabled(value, text) {
return !value || text !== value;
},
}
@action
confirm() {
this.adminUserIndex.send("deleteAllPosts");
},
}
@action
close() {
this.send("closeModal");
},
});
}
}

View File

@ -1,6 +1,8 @@
import Controller from "@ember/controller";
import ModalFunctionality from "discourse/mixins/modal-functionality";
export default Controller.extend(ModalFunctionality, {
deletedPercentage: 0,
});
export default class AdminDeleteUserPostsProgressController extends Controller.extend(
ModalFunctionality
) {
deletedPercentage = 0;
}

View File

@ -1,13 +1,16 @@
import { action } from "@ember/object";
import { inject as service } from "@ember/service";
import { A } from "@ember/array";
import Controller from "@ember/controller";
import I18n from "I18n";
import ModalFunctionality from "discourse/mixins/modal-functionality";
import { ajax } from "discourse/lib/ajax";
import { observes } from "discourse-common/utils/decorators";
import { inject as service } from "@ember/service";
export default Controller.extend(ModalFunctionality, {
dialog: service(),
export default class AdminEditBadgeGroupingsController extends Controller.extend(
ModalFunctionality
) {
@service dialog;
@observes("model")
modelChanged() {
@ -22,7 +25,7 @@ export default Controller.extend(ModalFunctionality, {
}
this.set("workingCopy", copy);
},
}
moveItem(item, delta) {
const copy = this.workingCopy;
@ -33,55 +36,68 @@ export default Controller.extend(ModalFunctionality, {
copy.removeAt(index);
copy.insertAt(index + delta, item);
},
}
actions: {
up(item) {
this.moveItem(item, -1);
},
down(item) {
this.moveItem(item, 1);
},
delete(item) {
this.workingCopy.removeObject(item);
},
cancel() {
this.setProperties({ model: null, workingCopy: null });
this.send("closeModal");
},
edit(item) {
item.set("editing", true);
},
save(item) {
item.set("editing", false);
},
add() {
const obj = this.store.createRecord("badge-grouping", {
editing: true,
name: I18n.t("admin.badges.badge_grouping"),
});
this.workingCopy.pushObject(obj);
},
saveAll() {
let items = this.workingCopy;
const groupIds = items.map((i) => i.get("id") || -1);
const names = items.map((i) => i.get("name"));
@action
up(item) {
this.moveItem(item, -1);
}
ajax("/admin/badges/badge_groupings", {
data: { ids: groupIds, names },
type: "POST",
}).then(
(data) => {
items = this.model;
items.clear();
data.badge_groupings.forEach((g) => {
items.pushObject(this.store.createRecord("badge-grouping", g));
});
this.setProperties({ model: null, workingCopy: null });
this.send("closeModal");
},
() => this.dialog.alert(I18n.t("generic_error"))
);
},
},
});
@action
down(item) {
this.moveItem(item, 1);
}
@action
delete(item) {
this.workingCopy.removeObject(item);
}
@action
cancel() {
this.setProperties({ model: null, workingCopy: null });
this.send("closeModal");
}
@action
edit(item) {
item.set("editing", true);
}
@action
save(item) {
item.set("editing", false);
}
@action
add() {
const obj = this.store.createRecord("badge-grouping", {
editing: true,
name: I18n.t("admin.badges.badge_grouping"),
});
this.workingCopy.pushObject(obj);
}
@action
saveAll() {
let items = this.workingCopy;
const groupIds = items.map((i) => i.get("id") || -1);
const names = items.map((i) => i.get("name"));
ajax("/admin/badges/badge_groupings", {
data: { ids: groupIds, names },
type: "POST",
}).then(
(data) => {
items = this.model;
items.clear();
data.badge_groupings.forEach((g) => {
items.pushObject(this.store.createRecord("badge-grouping", g));
});
this.setProperties({ model: null, workingCopy: null });
this.send("closeModal");
},
() => this.dialog.alert(I18n.t("generic_error"))
);
}
}

View File

@ -5,15 +5,17 @@ import discourseComputed from "discourse-common/utils/decorators";
import { longDate } from "discourse/lib/formatter";
import { popupAjaxError } from "discourse/lib/ajax-error";
export default Controller.extend(ModalFunctionality, {
export default class AdminIncomingEmailController extends Controller.extend(
ModalFunctionality
) {
@discourseComputed("model.date")
date(d) {
return longDate(d);
},
}
load(id) {
return IncomingEmail.find(id).then((result) => this.set("model", result));
},
}
loadFromBounced(id) {
return IncomingEmail.findByBounced(id)
@ -22,5 +24,5 @@ export default Controller.extend(ModalFunctionality, {
this.send("closeModal");
popupAjaxError(error);
});
},
});
}
}

View File

@ -1,46 +1,46 @@
import { alias, equal, match } from "@ember/object/computed";
import { COMPONENTS, THEMES } from "admin/models/theme";
import Controller, { inject as controller } from "@ember/controller";
import { alias, equal, match } from "@ember/object/computed";
import discourseComputed, { observes } from "discourse-common/utils/decorators";
import discourseComputed from "discourse-common/utils/decorators";
import { observes } from "@ember-decorators/object";
import I18n from "I18n";
import ModalFunctionality from "discourse/mixins/modal-functionality";
import { POPULAR_THEMES } from "discourse-common/helpers/popular-themes";
import { ajax } from "discourse/lib/ajax";
import { popupAjaxError } from "discourse/lib/ajax-error";
import { set } from "@ember/object";
import { action, set } from "@ember/object";
const MIN_NAME_LENGTH = 4;
export default Controller.extend(ModalFunctionality, {
adminCustomizeThemes: controller(),
themesController: controller("adminCustomizeThemes"),
popular: equal("selection", "popular"),
local: equal("selection", "local"),
remote: equal("selection", "remote"),
create: equal("selection", "create"),
directRepoInstall: equal("selection", "directRepoInstall"),
selection: "popular",
loading: false,
keyGenUrl: "/admin/themes/generate_key_pair",
importUrl: "/admin/themes/import",
recordType: "theme",
checkPrivate: match("uploadUrl", /^ssh:\/\/.+@.+$|.+@.+:.+$/),
localFile: null,
uploadUrl: null,
uploadName: null,
advancedVisible: false,
selectedType: alias("themesController.currentTab"),
component: equal("selectedType", COMPONENTS),
urlPlaceholder: "https://github.com/discourse/sample_theme",
export default class AdminInstallThemeController extends Controller.extend(
ModalFunctionality
) {
@controller adminCustomizeThemes;
@controller("adminCustomizeThemes") themesController;
init() {
this._super(...arguments);
@equal("selection", "popular") popular;
@equal("selection", "local") local;
@equal("selection", "remote") remote;
@equal("selection", "create") create;
@equal("selection", "directRepoInstall") directRepoInstall;
selection = "popular";
loading = false;
keyGenUrl = "/admin/themes/generate_key_pair";
importUrl = "/admin/themes/import";
recordType = "theme";
@match("uploadUrl", /^ssh:\/\/.+@.+$|.+@.+:.+$/) checkPrivate;
localFile = null;
uploadUrl = null;
uploadName = null;
advancedVisible = false;
@alias("themesController.currentTab") selectedType;
@equal("selectedType", COMPONENTS) component;
urlPlaceholder = "https://github.com/discourse/sample_theme";
this.createTypes = [
{ name: I18n.t("admin.customize.theme.theme"), value: THEMES },
{ name: I18n.t("admin.customize.theme.component"), value: COMPONENTS },
];
},
createTypes = [
{ name: I18n.t("admin.customize.theme.theme"), value: THEMES },
{ name: I18n.t("admin.customize.theme.component"), value: COMPONENTS },
];
@discourseComputed("themesController.installedThemes")
themes(installedThemes) {
@ -52,7 +52,7 @@ export default Controller.extend(ModalFunctionality, {
}
return t;
});
},
}
@discourseComputed(
"loading",
@ -78,12 +78,12 @@ export default Controller.extend(ModalFunctionality, {
(isLocal && !localFile) ||
(isCreate && nameTooShort)
);
},
}
@discourseComputed("name")
nameTooShort(name) {
return !name || name.length < MIN_NAME_LENGTH;
},
}
@discourseComputed("component")
placeholder(component) {
@ -92,7 +92,7 @@ export default Controller.extend(ModalFunctionality, {
} else {
return I18n.t("admin.customize.theme.theme_name");
}
},
}
@observes("checkPrivate")
privateWasChecked() {
@ -108,7 +108,7 @@ export default Controller.extend(ModalFunctionality, {
this._keyLoading = false;
});
}
},
}
@discourseComputed("selection", "themeCannotBeInstalled")
submitLabel(selection, themeCannotBeInstalled) {
@ -119,12 +119,12 @@ export default Controller.extend(ModalFunctionality, {
return `admin.customize.theme.${
selection === "create" ? "create" : "install"
}`;
},
}
@discourseComputed("checkPrivate", "publicKey")
showPublicKey(checkPrivate, publicKey) {
return checkPrivate && publicKey;
},
}
onClose() {
this.setProperties({
@ -140,7 +140,7 @@ export default Controller.extend(ModalFunctionality, {
repoName: null,
repoUrl: null,
});
},
}
themeHasSameUrl(theme, url) {
const themeUrl = theme.remote_theme && theme.remote_theme.remote_url;
@ -149,100 +149,101 @@ export default Controller.extend(ModalFunctionality, {
url &&
url.replace(/\.git$/, "") === themeUrl.replace(/\.git$/, "")
);
},
}
actions: {
uploadLocaleFile() {
this.set("localFile", $("#file-input")[0].files[0]);
},
@action
uploadLocaleFile() {
this.set("localFile", $("#file-input")[0].files[0]);
}
toggleAdvanced() {
this.toggleProperty("advancedVisible");
},
@action
toggleAdvanced() {
this.toggleProperty("advancedVisible");
}
installThemeFromList(url) {
this.set("uploadUrl", url);
this.send("installTheme");
},
installTheme() {
if (this.create) {
this.set("loading", true);
const theme = this.store.createRecord(this.recordType);
theme
.save({ name: this.name, component: this.component })
.then(() => {
this.themesController.send("addTheme", theme);
this.send("closeModal");
})
.catch(popupAjaxError)
.finally(() => this.set("loading", false));
return;
}
let options = {
type: "POST",
};
if (this.local) {
options.processData = false;
options.contentType = false;
options.data = new FormData();
options.data.append("theme", this.localFile);
}
if (this.remote || this.popular || this.directRepoInstall) {
const duplicate = this.themesController.model.content.find((theme) =>
this.themeHasSameUrl(theme, this.uploadUrl)
);
if (duplicate && !this.duplicateRemoteThemeWarning) {
const warning = I18n.t(
"admin.customize.theme.duplicate_remote_theme",
{ name: duplicate.name }
);
this.set("duplicateRemoteThemeWarning", warning);
return;
}
options.data = {
remote: this.uploadUrl,
branch: this.branch,
public_key: this.publicKey,
};
}
// User knows that theme cannot be installed, but they want to continue
// to force install it.
if (this.themeCannotBeInstalled) {
options.data["force"] = true;
}
if (this.get("model.user_id")) {
// Used by theme-creator
options.data["user_id"] = this.get("model.user_id");
}
@action
installThemeFromList(url) {
this.set("uploadUrl", url);
this.send("installTheme");
}
@action
installTheme() {
if (this.create) {
this.set("loading", true);
ajax(this.importUrl, options)
.then((result) => {
const theme = this.store.createRecord(this.recordType, result.theme);
this.adminCustomizeThemes.send("addTheme", theme);
const theme = this.store.createRecord(this.recordType);
theme
.save({ name: this.name, component: this.component })
.then(() => {
this.themesController.send("addTheme", theme);
this.send("closeModal");
})
.then(() => {
this.set("publicKey", null);
})
.catch((error) => {
if (!this.publicKey || this.themeCannotBeInstalled) {
return popupAjaxError(error);
}
this.set(
"themeCannotBeInstalled",
I18n.t("admin.customize.theme.force_install")
);
})
.catch(popupAjaxError)
.finally(() => this.set("loading", false));
},
},
});
return;
}
let options = {
type: "POST",
};
if (this.local) {
options.processData = false;
options.contentType = false;
options.data = new FormData();
options.data.append("theme", this.localFile);
}
if (this.remote || this.popular || this.directRepoInstall) {
const duplicate = this.themesController.model.content.find((theme) =>
this.themeHasSameUrl(theme, this.uploadUrl)
);
if (duplicate && !this.duplicateRemoteThemeWarning) {
const warning = I18n.t("admin.customize.theme.duplicate_remote_theme", {
name: duplicate.name,
});
this.set("duplicateRemoteThemeWarning", warning);
return;
}
options.data = {
remote: this.uploadUrl,
branch: this.branch,
public_key: this.publicKey,
};
}
// User knows that theme cannot be installed, but they want to continue
// to force install it.
if (this.themeCannotBeInstalled) {
options.data["force"] = true;
}
if (this.get("model.user_id")) {
// Used by theme-creator
options.data["user_id"] = this.get("model.user_id");
}
this.set("loading", true);
ajax(this.importUrl, options)
.then((result) => {
const theme = this.store.createRecord(this.recordType, result.theme);
this.adminCustomizeThemes.send("addTheme", theme);
this.send("closeModal");
})
.then(() => {
this.set("publicKey", null);
})
.catch((error) => {
if (!this.publicKey || this.themeCannotBeInstalled) {
return popupAjaxError(error);
}
this.set(
"themeCannotBeInstalled",
I18n.t("admin.customize.theme.force_install")
);
})
.finally(() => this.set("loading", false));
}
}

View File

@ -1,18 +1,21 @@
import { alias } from "@ember/object/computed";
import Controller, { inject as controller } from "@ember/controller";
import I18n from "I18n";
import ModalFunctionality from "discourse/mixins/modal-functionality";
import { action } from "@ember/object";
import { alias } from "@ember/object/computed";
import discourseComputed from "discourse-common/utils/decorators";
export default Controller.extend(ModalFunctionality, {
adminUserIndex: controller(),
username: alias("model.username"),
targetUsername: alias("model.targetUsername"),
export default class AdminMergeUsersConfirmationController extends Controller.extend(
ModalFunctionality
) {
@controller adminUserIndex;
@alias("model.username") username;
@alias("model.targetUsername") targetUsername;
onShow() {
this.set("value", null);
},
}
@discourseComputed("username", "targetUsername")
text(username, targetUsername) {
@ -20,28 +23,28 @@ export default Controller.extend(ModalFunctionality, {
username,
targetUsername,
});
},
}
@discourseComputed("username")
mergeButtonText(username) {
return I18n.t(`admin.user.merge.confirmation.transfer_and_delete`, {
username,
});
},
}
@discourseComputed("value", "text")
mergeDisabled(value, text) {
return !value || text !== value;
},
}
@action
confirm() {
this.adminUserIndex.send("merge", this.targetUsername);
this.send("closeModal");
},
}
@action
close() {
this.send("closeModal");
},
});
}
}

View File

@ -4,16 +4,18 @@ import I18n from "I18n";
import ModalFunctionality from "discourse/mixins/modal-functionality";
import { bind } from "discourse-common/utils/decorators";
export default Controller.extend(ModalFunctionality, {
message: I18n.t("admin.user.merging_user"),
export default class AdminMergeUsersProgressController extends Controller.extend(
ModalFunctionality
) {
message = I18n.t("admin.user.merging_user");
onShow() {
this.messageBus.subscribe("/merge_user", this.onMessage);
},
}
onClose() {
this.messageBus.unsubscribe("/merge_user", this.onMessage);
},
}
@bind
onMessage(data) {
@ -30,5 +32,5 @@ export default Controller.extend(ModalFunctionality, {
} else if (data.failed) {
this.set("message", I18n.t("admin.user.merge_failed"));
}
},
});
}
}

View File

@ -1,43 +1,46 @@
import { alias } from "@ember/object/computed";
import Controller, { inject as controller } from "@ember/controller";
import I18n from "I18n";
import ModalFunctionality from "discourse/mixins/modal-functionality";
import { action, get } from "@ember/object";
import { alias } from "@ember/object/computed";
import discourseComputed from "discourse-common/utils/decorators";
export default Controller.extend(ModalFunctionality, {
adminUserIndex: controller(),
username: alias("model.username"),
export default class AdminMergeUsersPromptController extends Controller.extend(
ModalFunctionality
) {
@controller adminUserIndex;
@alias("model.username") username;
onShow() {
this.set("targetUsername", null);
},
}
@discourseComputed("username", "targetUsername")
mergeDisabled(username, targetUsername) {
return !targetUsername || username === targetUsername;
},
}
@discourseComputed("username")
mergeButtonText(username) {
return I18n.t(`admin.user.merge.confirmation.transfer_and_delete`, {
username,
});
},
}
@action
showConfirmation() {
this.send("closeModal");
this.adminUserIndex.send("showMergeConfirmation", this.targetUsername);
},
}
@action
close() {
this.send("closeModal");
},
}
@action
updateUsername(selected) {
this.set("targetUsername", get(selected, "firstObject"));
},
});
}
}

View File

@ -1,29 +1,31 @@
import { inject as service } from "@ember/service";
import Controller from "@ember/controller";
import { action } from "@ember/object";
import { next } from "@ember/runloop";
import { inject as service } from "@ember/service";
import { isEmpty } from "@ember/utils";
import discourseComputed from "discourse-common/utils/decorators";
import { extractError } from "discourse/lib/ajax-error";
import ModalFunctionality from "discourse/mixins/modal-functionality";
import I18n from "I18n";
export default Controller.extend(ModalFunctionality, {
dialog: service(),
export default class AdminPenalizeUserController extends Controller.extend(
ModalFunctionality
) {
@service dialog;
loadingUser: false,
errorMessage: null,
penaltyType: null,
penalizeUntil: null,
reason: null,
message: null,
postId: null,
postAction: null,
postEdit: null,
user: null,
otherUserIds: null,
loading: false,
confirmClose: false,
loadingUser = false;
errorMessage = null;
penaltyType = null;
penalizeUntil = null;
reason = null;
message = null;
postId = null;
postAction = null;
postEdit = null;
user = null;
otherUserIds = null;
loading = false;
confirmClose = false;
onShow() {
this.setProperties({
@ -44,11 +46,11 @@ export default Controller.extend(ModalFunctionality, {
message: null,
confirmClose: false,
});
},
}
finishedSetup() {
this.set("penalizeUntil", this.user?.next_penalty);
},
}
beforeClose() {
if (this.confirmClose) {
@ -73,7 +75,7 @@ export default Controller.extend(ModalFunctionality, {
return false;
}
},
}
@discourseComputed("penaltyType")
modalTitle(penaltyType) {
@ -82,7 +84,7 @@ export default Controller.extend(ModalFunctionality, {
} else if (penaltyType === "silence") {
return "admin.user.silence_modal_title";
}
},
}
@discourseComputed("penaltyType")
buttonLabel(penaltyType) {
@ -91,7 +93,7 @@ export default Controller.extend(ModalFunctionality, {
} else if (penaltyType === "silence") {
return "admin.user.silence";
}
},
}
@discourseComputed(
"user.penalty_counts.suspended",
@ -102,7 +104,7 @@ export default Controller.extend(ModalFunctionality, {
SUSPENDED: suspendedCount,
SILENCED: silencedCount,
});
},
}
@discourseComputed("penaltyType", "user.canSuspend", "user.canSilence")
canPenalize(penaltyType, canSuspend, canSilence) {
@ -113,12 +115,12 @@ export default Controller.extend(ModalFunctionality, {
}
return false;
},
}
@discourseComputed("penalizing", "penalizeUntil", "reason")
submitDisabled(penalizing, penalizeUntil, reason) {
return penalizing || isEmpty(penalizeUntil) || !reason || reason.length < 1;
},
}
@action
async penalizeUser() {
@ -164,5 +166,5 @@ export default Controller.extend(ModalFunctionality, {
} finally {
this.set("penalizing", false);
}
},
});
}
}

View File

@ -1,15 +1,19 @@
import { action } from "@ember/object";
import { inject as service } from "@ember/service";
import Controller from "@ember/controller";
import I18n from "I18n";
import ModalFunctionality from "discourse/mixins/modal-functionality";
import { ajax } from "discourse/lib/ajax";
import { inject as service } from "@ember/service";
export default Controller.extend(ModalFunctionality, {
dialog: service(),
loading: true,
reseeding: false,
categories: null,
topics: null,
export default class AdminReseedController extends Controller.extend(
ModalFunctionality
) {
@service dialog;
loading = true;
reseeding = false;
categories = null;
topics = null;
onShow() {
ajax("/admin/customize/reseed")
@ -20,27 +24,26 @@ export default Controller.extend(ModalFunctionality, {
});
})
.finally(() => this.set("loading", false));
},
}
_extractSelectedIds(items) {
return items.filter((item) => item.selected).map((item) => item.id);
},
}
actions: {
reseed() {
this.set("reseeding", true);
ajax("/admin/customize/reseed", {
data: {
category_ids: this._extractSelectedIds(this.categories),
topic_ids: this._extractSelectedIds(this.topics),
},
type: "POST",
})
.catch(() => this.dialog.alert(I18n.t("generic_error")))
.finally(() => {
this.set("reseeding", false);
this.send("closeModal");
});
},
},
});
@action
reseed() {
this.set("reseeding", true);
ajax("/admin/customize/reseed", {
data: {
category_ids: this._extractSelectedIds(this.categories),
topic_ids: this._extractSelectedIds(this.topics),
},
type: "POST",
})
.catch(() => this.dialog.alert(I18n.t("generic_error")))
.finally(() => {
this.set("reseeding", false);
this.send("closeModal");
});
}
}

View File

@ -1,4 +1,6 @@
import Controller from "@ember/controller";
import ModalFunctionality from "discourse/mixins/modal-functionality";
export default Controller.extend(ModalFunctionality);
export default class AdminStaffActionLogDetailsController extends Controller.extend(
ModalFunctionality
) {}

View File

@ -1,35 +1,39 @@
import { action } from "@ember/object";
import Controller, { inject as controller } from "@ember/controller";
import discourseComputed from "discourse-common/utils/decorators";
import ModalFunctionality from "discourse/mixins/modal-functionality";
export default Controller.extend(ModalFunctionality, {
adminBackupsLogs: controller(),
export default class AdminStartBackupController extends Controller.extend(
ModalFunctionality
) {
@controller adminBackupsLogs;
@discourseComputed
warningMessage() {
// this is never shown here, but we may want to show different
// messages in plugins
return "";
},
}
@discourseComputed
yesLabel() {
return "yes_value";
},
}
actions: {
startBackupWithUploads() {
this.send("closeModal");
this.send("startBackup", true);
},
@action
startBackupWithUploads() {
this.send("closeModal");
this.send("startBackup", true);
}
startBackupWithoutUploads() {
this.send("closeModal");
this.send("startBackup", false);
},
@action
startBackupWithoutUploads() {
this.send("closeModal");
this.send("startBackup", false);
}
cancel() {
this.send("closeModal");
},
},
});
@action
cancel() {
this.send("closeModal");
}
}

View File

@ -2,7 +2,9 @@ import Controller from "@ember/controller";
import ModalFunctionality from "discourse/mixins/modal-functionality";
import { ajax } from "discourse/lib/ajax";
export default Controller.extend(ModalFunctionality, {
export default class AdminThemeChangeController extends Controller.extend(
ModalFunctionality
) {
loadDiff() {
this.set("loading", true);
ajax(
@ -11,5 +13,5 @@ export default Controller.extend(ModalFunctionality, {
this.set("loading", false);
this.set("diff", diff.side_by_side);
});
},
});
}
}

View File

@ -1,30 +1,32 @@
import { observes, on } from "discourse-common/utils/decorators";
import { observes, on } from "@ember-decorators/object";
import Controller from "@ember/controller";
import { action } from "@ember/object";
import ModalFunctionality from "discourse/mixins/modal-functionality";
export default Controller.extend(ModalFunctionality, {
export default class AdminUploadedImageListController extends Controller.extend(
ModalFunctionality
) {
@on("init")
@observes("model.value")
_setup() {
const value = this.get("model.value");
this.set("images", value && value.length ? value.split("|") : []);
},
}
@action
remove(url, event) {
event?.preventDefault();
this.images.removeObject(url);
},
}
actions: {
uploadDone({ url }) {
this.images.addObject(url);
},
@action
uploadDone({ url }) {
this.images.addObject(url);
}
close() {
this.save(this.images.join("|"));
this.send("closeModal");
},
},
});
@action
close() {
this.save(this.images.join("|"));
this.send("closeModal");
}
}

View File

@ -1,16 +1,19 @@
import { equal } from "@ember/object/computed";
import Controller from "@ember/controller";
import ModalFunctionality from "discourse/mixins/modal-functionality";
import discourseComputed from "discourse-common/utils/decorators";
import { equal } from "@ember/object/computed";
import {
createWatchedWordRegExp,
toWatchedWord,
} from "discourse-common/utils/watched-words";
export default Controller.extend(ModalFunctionality, {
isReplace: equal("model.nameKey", "replace"),
isTag: equal("model.nameKey", "tag"),
isLink: equal("model.nameKey", "link"),
export default class AdminWatchedWordTestController extends Controller.extend(
ModalFunctionality
) {
@equal("model.nameKey", "replace") isReplace;
@equal("model.nameKey", "tag") isTag;
@equal("model.nameKey", "link") isLink;
@discourseComputed(
"value",
@ -71,5 +74,5 @@ export default Controller.extend(ModalFunctionality, {
return matches;
}
},
});
}
}

View File

@ -2,4 +2,7 @@ import Controller from "@ember/controller";
import ModalFunctionality from "discourse/mixins/modal-functionality";
import ModalUpdateExistingUsers from "discourse/mixins/modal-update-existing-users";
export default Controller.extend(ModalFunctionality, ModalUpdateExistingUsers);
export default class SiteSettingDefaultCategoriesController extends Controller.extend(
ModalFunctionality,
ModalUpdateExistingUsers
) {}

View File

@ -2,7 +2,7 @@ import Helper from "@ember/component/helper";
import { iconHTML } from "discourse-common/lib/icon-library";
import { htmlSafe } from "@ember/template";
export default Helper.extend({
export default class DispositionIcon extends Helper {
compute([disposition]) {
if (!disposition) {
return null;
@ -24,5 +24,5 @@ export default Helper.extend({
}
}
return htmlSafe(iconHTML(icon, { title }));
},
});
}
}

View File

@ -7,19 +7,17 @@ const GENERAL_ATTRIBUTES = [
"release_notes_link",
];
const AdminDashboard = EmberObject.extend({});
AdminDashboard.reopenClass({
fetch() {
export default class AdminDashboard extends EmberObject {
static fetch() {
return ajax("/admin/dashboard.json").then((json) => {
const model = AdminDashboard.create();
model.set("version_check", json.version_check);
return model;
});
},
}
fetchGeneral() {
static fetchGeneral() {
return ajax("/admin/dashboard/general.json").then((json) => {
const model = AdminDashboard.create();
@ -34,15 +32,13 @@ AdminDashboard.reopenClass({
return model;
});
},
}
fetchProblems() {
static fetchProblems() {
return ajax("/admin/dashboard/problems.json").then((json) => {
const model = AdminDashboard.create(json);
model.set("loaded", true);
return model;
});
},
});
export default AdminDashboard;
}
}

View File

@ -10,14 +10,30 @@ import { popupAjaxError } from "discourse/lib/ajax-error";
import { propertyNotEqual } from "discourse/lib/computed";
import { userPath } from "discourse/lib/url";
const wrapAdmin = (user) => (user ? AdminUser.create(user) : null);
export default class AdminUser extends User {
static find(user_id) {
return ajax(`/admin/users/${user_id}.json`).then((result) => {
result.loadedDetails = true;
return AdminUser.create(result);
});
}
const AdminUser = User.extend({
adminUserView: true,
customGroups: filter("groups", (g) => !g.automatic && Group.create(g)),
automaticGroups: filter("groups", (g) => g.automatic && Group.create(g)),
static findAll(query, userFilter) {
return ajax(`/admin/users/list/${query}.json`, {
data: userFilter,
}).then((users) => users.map((u) => AdminUser.create(u)));
}
canViewProfile: or("active", "staged"),
adminUserView = true;
@filter("groups", (g) => !g.automatic && Group.create(g)) customGroups;
@filter("groups", (g) => g.automatic && Group.create(g)) automaticGroups;
@or("active", "staged") canViewProfile;
@gt("bounce_score", 0) canResetBounceScore;
@propertyNotEqual("originalTrustLevel", "trust_level") dirty;
@lt("trust_level", 4) canLockTrustLevel;
@not("staff") canSuspend;
@not("staff") canSilence;
@discourseComputed("bounce_score", "reset_bounce_score_after")
bounceScore(bounce_score, reset_bounce_score_after) {
@ -28,7 +44,7 @@ const AdminUser = User.extend({
} else {
return bounce_score;
}
},
}
@discourseComputed("bounce_score")
bounceScoreExplanation(bounce_score) {
@ -39,14 +55,12 @@ const AdminUser = User.extend({
} else {
return I18n.t("admin.user.bounce_score_explanation.threshold_reached");
}
},
}
@discourseComputed
bounceLink() {
return getURL("/admin/email/bounced");
},
canResetBounceScore: gt("bounce_score", 0),
}
resetBounceScore() {
return ajax(`/admin/users/${this.id}/reset_bounce_score`, {
@ -57,14 +71,14 @@ const AdminUser = User.extend({
reset_bounce_score_after: null,
})
);
},
}
groupAdded(added) {
return ajax(`/admin/users/${this.id}/groups`, {
type: "POST",
data: { group_id: added.id },
}).then(() => this.groups.pushObject(added));
},
}
groupRemoved(groupId) {
return ajax(`/admin/users/${this.id}/groups/${groupId}`, {
@ -75,13 +89,13 @@ const AdminUser = User.extend({
this.set("primary_group_id", null);
}
});
},
}
deleteAllPosts() {
return ajax(`/admin/users/${this.get("id")}/delete_posts_batch`, {
type: "PUT",
});
},
}
revokeAdmin() {
return ajax(`/admin/users/${this.id}/revoke_admin`, {
@ -97,7 +111,7 @@ const AdminUser = User.extend({
can_delete_all_posts: resp.can_delete_all_posts,
});
});
},
}
grantAdmin(data) {
return ajax(`/admin/users/${this.id}/grant_admin`, {
@ -114,7 +128,7 @@ const AdminUser = User.extend({
return resp;
});
},
}
revokeModeration() {
return ajax(`/admin/users/${this.id}/revoke_moderation`, {
@ -130,7 +144,7 @@ const AdminUser = User.extend({
});
})
.catch(popupAjaxError);
},
}
grantModeration() {
return ajax(`/admin/users/${this.id}/grant_moderation`, {
@ -146,7 +160,7 @@ const AdminUser = User.extend({
});
})
.catch(popupAjaxError);
},
}
disableSecondFactor() {
return ajax(`/admin/users/${this.id}/disable_second_factor`, {
@ -156,7 +170,7 @@ const AdminUser = User.extend({
this.set("second_factor_enabled", false);
})
.catch(popupAjaxError);
},
}
approve(approvedBy) {
return ajax(`/admin/users/${this.id}/approve`, {
@ -168,83 +182,76 @@ const AdminUser = User.extend({
approved_by: approvedBy,
});
});
},
}
setOriginalTrustLevel() {
this.set("originalTrustLevel", this.trust_level);
},
dirty: propertyNotEqual("originalTrustLevel", "trust_level"),
}
saveTrustLevel() {
return ajax(`/admin/users/${this.id}/trust_level`, {
type: "PUT",
data: { level: this.trust_level },
});
},
}
restoreTrustLevel() {
this.set("trust_level", this.originalTrustLevel);
},
}
lockTrustLevel(locked) {
return ajax(`/admin/users/${this.id}/trust_level_lock`, {
type: "PUT",
data: { locked: !!locked },
});
},
canLockTrustLevel: lt("trust_level", 4),
canSuspend: not("staff"),
canSilence: not("staff"),
}
@discourseComputed("suspended_till", "suspended_at")
suspendDuration(suspendedTill, suspendedAt) {
suspendedAt = moment(suspendedAt);
suspendedTill = moment(suspendedTill);
return suspendedAt.format("L") + " - " + suspendedTill.format("L");
},
}
suspend(data) {
return ajax(`/admin/users/${this.id}/suspend`, {
type: "PUT",
data,
}).then((result) => this.setProperties(result.suspension));
},
}
unsuspend() {
return ajax(`/admin/users/${this.id}/unsuspend`, {
type: "PUT",
}).then((result) => this.setProperties(result.suspension));
},
}
logOut() {
return ajax("/admin/users/" + this.id + "/log_out", {
type: "POST",
data: { username_or_email: this.username },
});
},
}
impersonate() {
return ajax("/admin/impersonate", {
type: "POST",
data: { username_or_email: this.username },
});
},
}
activate() {
return ajax(`/admin/users/${this.id}/activate`, {
type: "PUT",
});
},
}
deactivate() {
return ajax(`/admin/users/${this.id}/deactivate`, {
type: "PUT",
data: { context: document.location.pathname },
});
},
}
unsilence() {
this.set("silencingUser", true);
@ -254,7 +261,7 @@ const AdminUser = User.extend({
})
.then((result) => this.setProperties(result.unsilence))
.finally(() => this.set("silencingUser", false));
},
}
silence(data) {
this.set("silencingUser", true);
@ -265,20 +272,20 @@ const AdminUser = User.extend({
})
.then((result) => this.setProperties(result.silence))
.finally(() => this.set("silencingUser", false));
},
}
sendActivationEmail() {
return ajax(userPath("action/send_activation_email"), {
type: "POST",
data: { username: this.username },
});
},
}
anonymize() {
return ajax(`/admin/users/${this.id}/anonymize.json`, {
type: "PUT",
});
},
}
destroy(formData) {
return ajax(`/admin/users/${this.id}.json`, {
@ -295,14 +302,14 @@ const AdminUser = User.extend({
.catch(() => {
this.find(this.id).then((u) => this.setProperties(u));
});
},
}
merge(formData) {
return ajax(`/admin/users/${this.id}/merge.json`, {
type: "POST",
data: formData,
});
},
}
loadDetails() {
if (this.loadedDetails) {
@ -313,23 +320,29 @@ const AdminUser = User.extend({
const userProperties = Object.assign(result, { loadedDetails: true });
this.setProperties(userProperties);
});
},
}
@discourseComputed("tl3_requirements")
tl3Requirements(requirements) {
if (requirements) {
return this.store.createRecord("tl3Requirements", requirements);
}
},
}
@discourseComputed("suspended_by")
suspendedBy: wrapAdmin,
suspendedBy(user) {
return user ? AdminUser.create(user) : null;
}
@discourseComputed("silenced_by")
silencedBy: wrapAdmin,
silencedBy(user) {
return user ? AdminUser.create(user) : null;
}
@discourseComputed("approved_by")
approvedBy: wrapAdmin,
approvedBy(user) {
return user ? AdminUser.create(user) : null;
}
deleteSSORecord() {
return ajax(`/admin/users/${this.id}/sso_record.json`, {
@ -339,22 +352,5 @@ const AdminUser = User.extend({
this.set("single_sign_on_record", null);
})
.catch(popupAjaxError);
},
});
AdminUser.reopenClass({
find(user_id) {
return ajax(`/admin/users/${user_id}.json`).then((result) => {
result.loadedDetails = true;
return AdminUser.create(result);
});
},
findAll(query, userFilter) {
return ajax(`/admin/users/list/${query}.json`, {
data: userFilter,
}).then((users) => users.map((u) => AdminUser.create(u)));
},
});
export default AdminUser;
}
}

View File

@ -1,24 +1,26 @@
import { computed } from "@ember/object";
import AdminUser from "admin/models/admin-user";
import RestModel from "discourse/models/rest";
import { ajax } from "discourse/lib/ajax";
import { computed } from "@ember/object";
import discourseComputed from "discourse-common/utils/decorators";
import { fmt } from "discourse/lib/computed";
const ApiKey = RestModel.extend({
user: computed("_user", {
get() {
return this._user;
},
set(key, value) {
if (value && !(value instanceof AdminUser)) {
this.set("_user", AdminUser.create(value));
} else {
this.set("_user", value);
}
return this._user;
},
}),
export default class ApiKey extends RestModel {
@fmt("truncated_key", "%@...") truncatedKey;
@computed("_user")
get user() {
return this._user;
}
set user(value) {
if (value && !(value instanceof AdminUser)) {
this.set("_user", AdminUser.create(value));
} else {
this.set("_user", value);
}
return this._user;
}
@discourseComputed("description")
shortDescription(description) {
@ -26,32 +28,28 @@ const ApiKey = RestModel.extend({
return description;
}
return `${description.substring(0, 40)}...`;
},
truncatedKey: fmt("truncated_key", "%@..."),
}
revoke() {
return ajax(`${this.basePath}/revoke`, {
type: "POST",
}).then((result) => this.setProperties(result.api_key));
},
}
undoRevoke() {
return ajax(`${this.basePath}/undo-revoke`, {
type: "POST",
}).then((result) => this.setProperties(result.api_key));
},
}
createProperties() {
return this.getProperties("description", "username", "scopes");
},
}
@discourseComputed()
basePath() {
return this.store
.adapterFor("api-key")
.pathFor(this.store, "api-key", this.id);
},
});
export default ApiKey;
}
}

View File

@ -1,12 +1,12 @@
import { not } from "@ember/object/computed";
import EmberObject from "@ember/object";
import discourseComputed from "discourse-common/utils/decorators";
import { not } from "@ember/object/computed";
export default EmberObject.extend({
restoreDisabled: not("restoreEnabled"),
export default class BackupStatus extends EmberObject {
@not("restoreEnabled") restoreDisabled;
@discourseComputed("allowRestore", "isOperationRunning")
restoreEnabled(allowRestore, isOperationRunning) {
return allowRestore && !isOperationRunning;
},
});
}
}

View File

@ -2,25 +2,12 @@ import EmberObject from "@ember/object";
import MessageBus from "message-bus-client";
import { ajax } from "discourse/lib/ajax";
const Backup = EmberObject.extend({
destroy() {
return ajax("/admin/backups/" + this.filename, { type: "DELETE" });
},
restore() {
return ajax("/admin/backups/" + this.filename + "/restore", {
type: "POST",
data: { client_id: MessageBus.clientId },
});
},
});
Backup.reopenClass({
find() {
export default class Backup extends EmberObject {
static find() {
return ajax("/admin/backups.json");
},
}
start(withUploads) {
static start(withUploads) {
if (withUploads === undefined) {
withUploads = true;
}
@ -31,19 +18,28 @@ Backup.reopenClass({
client_id: MessageBus.clientId,
},
});
},
}
cancel() {
static cancel() {
return ajax("/admin/backups/cancel.json", {
type: "DELETE",
});
},
}
rollback() {
static rollback() {
return ajax("/admin/backups/rollback.json", {
type: "POST",
});
},
});
}
export default Backup;
destroy() {
return ajax("/admin/backups/" + this.filename, { type: "DELETE" });
}
restore() {
return ajax("/admin/backups/" + this.filename + "/restore", {
type: "POST",
data: { client_id: MessageBus.clientId },
});
}
}

View File

@ -1,19 +1,19 @@
import discourseComputed, {
observes,
on,
} from "discourse-common/utils/decorators";
import discourseComputed from "discourse-common/utils/decorators";
import { observes, on } from "@ember-decorators/object";
import EmberObject from "@ember/object";
import I18n from "I18n";
import { propertyNotEqual } from "discourse/lib/computed";
const ColorSchemeColor = EmberObject.extend({
export default class ColorSchemeColor extends EmberObject {
// Whether the current value is different than Discourse's default color scheme.
@propertyNotEqual("hex", "default_hex") overridden;
@on("init")
startTrackingChanges() {
this.set("originals", { hex: this.hex || "FFFFFF" });
// force changed property to be recalculated
this.notifyPropertyChange("hex");
},
}
// Whether value has changed since it was last saved.
@discourseComputed("hex")
@ -26,26 +26,23 @@ const ColorSchemeColor = EmberObject.extend({
}
return false;
},
// Whether the current value is different than Discourse's default color scheme.
overridden: propertyNotEqual("hex", "default_hex"),
}
// Whether the saved value is different than Discourse's default color scheme.
@discourseComputed("default_hex", "hex")
savedIsOverriden(defaultHex) {
return this.originals.hex !== defaultHex;
},
}
revert() {
this.set("hex", this.default_hex);
},
}
undo() {
if (this.originals) {
this.set("hex", this.originals.hex);
}
},
}
@discourseComputed("name")
translatedName(name) {
@ -54,7 +51,7 @@ const ColorSchemeColor = EmberObject.extend({
} else {
return name;
}
},
}
@discourseComputed("name")
description(name) {
@ -63,7 +60,7 @@ const ColorSchemeColor = EmberObject.extend({
} else {
return "";
}
},
}
/**
brightness returns a number between 0 (darkest) to 255 (brightest).
@ -90,19 +87,17 @@ const ColorSchemeColor = EmberObject.extend({
1000
);
}
},
}
@observes("hex")
hexValueChanged() {
if (this.hex) {
this.set("hex", this.hex.toString().replace(/[^0-9a-fA-F]/g, ""));
}
},
}
@discourseComputed("hex")
valid(hex) {
return hex.match(/^([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/) !== null;
},
});
export default ColorSchemeColor;
}
}

View File

@ -1,3 +1,4 @@
import { not } from "@ember/object/computed";
import { A } from "@ember/array";
import ArrayProxy from "@ember/array/proxy";
import ColorSchemeColor from "admin/models/color-scheme-color";
@ -5,26 +6,56 @@ import EmberObject from "@ember/object";
import I18n from "I18n";
import { ajax } from "discourse/lib/ajax";
import discourseComputed from "discourse-common/utils/decorators";
import { not } from "@ember/object/computed";
const ColorScheme = EmberObject.extend({
class ColorSchemes extends ArrayProxy {}
export default class ColorScheme extends EmberObject {
static findAll() {
const colorSchemes = ColorSchemes.create({ content: [], loading: true });
return ajax("/admin/color_schemes").then((all) => {
all.forEach((colorScheme) => {
colorSchemes.pushObject(
ColorScheme.create({
id: colorScheme.id,
name: colorScheme.name,
is_base: colorScheme.is_base,
theme_id: colorScheme.theme_id,
theme_name: colorScheme.theme_name,
base_scheme_id: colorScheme.base_scheme_id,
user_selectable: colorScheme.user_selectable,
colors: colorScheme.colors.map((c) => {
return ColorSchemeColor.create({
name: c.name,
hex: c.hex,
default_hex: c.default_hex,
is_advanced: c.is_advanced,
});
}),
})
);
});
return colorSchemes;
});
}
@not("id") newRecord;
init() {
this._super(...arguments);
super.init(...arguments);
this.startTrackingChanges();
},
}
@discourseComputed
description() {
return "" + this.name;
},
}
startTrackingChanges() {
this.set("originals", {
name: this.name,
user_selectable: this.user_selectable,
});
},
}
schemeJson() {
const buffer = [];
@ -33,7 +64,7 @@ const ColorScheme = EmberObject.extend({
});
return [`"${this.name}": {`, buffer.join(",\n"), "}"].join("\n");
},
}
copy() {
const newScheme = ColorScheme.create({
@ -47,7 +78,7 @@ const ColorScheme = EmberObject.extend({
);
});
return newScheme;
},
}
@discourseComputed(
"name",
@ -70,7 +101,7 @@ const ColorScheme = EmberObject.extend({
}
return false;
},
}
@discourseComputed("changed")
disableSave(changed) {
@ -79,9 +110,7 @@ const ColorScheme = EmberObject.extend({
}
return !changed || this.saving || this.colors.any((c) => !c.get("valid"));
},
newRecord: not("id"),
}
save(opts) {
if (this.is_base || this.disableSave) {
@ -124,7 +153,7 @@ const ColorScheme = EmberObject.extend({
this.setProperties({ savingStatus: I18n.t("saved"), saving: false });
this.notifyPropertyChange("description");
});
},
}
updateUserSelectable(value) {
if (!this.id) {
@ -137,45 +166,11 @@ const ColorScheme = EmberObject.extend({
dataType: "json",
contentType: "application/json",
});
},
}
destroy() {
if (this.id) {
return ajax(`/admin/color_schemes/${this.id}`, { type: "DELETE" });
}
},
});
const ColorSchemes = ArrayProxy.extend({});
ColorScheme.reopenClass({
findAll() {
const colorSchemes = ColorSchemes.create({ content: [], loading: true });
return ajax("/admin/color_schemes").then((all) => {
all.forEach((colorScheme) => {
colorSchemes.pushObject(
ColorScheme.create({
id: colorScheme.id,
name: colorScheme.name,
is_base: colorScheme.is_base,
theme_id: colorScheme.theme_id,
theme_name: colorScheme.theme_name,
base_scheme_id: colorScheme.base_scheme_id,
user_selectable: colorScheme.user_selectable,
colors: colorScheme.colors.map((c) => {
return ColorSchemeColor.create({
name: c.name,
hex: c.hex,
default_hex: c.default_hex,
is_advanced: c.is_advanced,
});
}),
})
);
});
return colorSchemes;
});
},
});
export default ColorScheme;
}
}

View File

@ -3,10 +3,8 @@ import EmberObject from "@ember/object";
import { ajax } from "discourse/lib/ajax";
import getURL from "discourse-common/lib/get-url";
const EmailLog = EmberObject.extend({});
EmailLog.reopenClass({
create(attrs) {
export default class EmailLog extends EmberObject {
static create(attrs) {
attrs = attrs || {};
if (attrs.user) {
@ -17,10 +15,10 @@ EmailLog.reopenClass({
attrs.post_url = getURL(attrs.post_url);
}
return this._super(attrs);
},
return super.create(attrs);
}
findAll(filter, offset) {
static findAll(filter, offset) {
filter = filter || {};
offset = offset || 0;
@ -30,7 +28,5 @@ EmailLog.reopenClass({
return ajax(`/admin/email/${status}.json?offset=${offset}`, {
data: filter,
}).then((logs) => logs.map((log) => EmailLog.create(log)));
},
});
export default EmailLog;
}
}

View File

@ -1,25 +1,21 @@
import EmberObject from "@ember/object";
import { ajax } from "discourse/lib/ajax";
const EmailPreview = EmberObject.extend({});
export function oneWeekAgo() {
return moment().locale("en").subtract(7, "days").format("YYYY-MM-DD");
}
EmailPreview.reopenClass({
findDigest(username, lastSeenAt) {
export default class EmailPreview extends EmberObject {
static findDigest(username, lastSeenAt) {
return ajax("/admin/email/preview-digest.json", {
data: { last_seen_at: lastSeenAt || oneWeekAgo(), username },
}).then((result) => EmailPreview.create(result));
},
}
sendDigest(username, lastSeenAt, email) {
static sendDigest(username, lastSeenAt, email) {
return ajax("/admin/email/send-digest.json", {
type: "POST",
data: { last_seen_at: lastSeenAt || oneWeekAgo(), username, email },
});
},
});
}
}
export default EmailPreview;
export function oneWeekAgo() {
return moment().locale("en").subtract(7, "days").format("YYYY-MM-DD");
}

View File

@ -1,14 +1,10 @@
import EmberObject from "@ember/object";
import { ajax } from "discourse/lib/ajax";
const EmailSettings = EmberObject.extend({});
EmailSettings.reopenClass({
find() {
export default class EmailSettings extends EmberObject {
static find() {
return ajax("/admin/email.json").then(function (settings) {
return EmailSettings.create(settings);
});
},
});
export default EmailSettings;
}
}

View File

@ -1,10 +1,10 @@
import RestModel from "discourse/models/rest";
export default RestModel.extend({
changed: false,
export default class EmailStyle extends RestModel {
changed = false;
setField(fieldName, value) {
this.set(`${fieldName}`, value);
this.set("changed", true);
},
});
}
}

View File

@ -2,12 +2,12 @@ import RestModel from "discourse/models/rest";
import { ajax } from "discourse/lib/ajax";
import { getProperties } from "@ember/object";
export default RestModel.extend({
export default class EmailTemplate extends RestModel {
revert() {
return ajax(`/admin/customize/email_templates/${this.id}`, {
type: "DELETE",
}).then((result) =>
getProperties(result.email_template, "subject", "body", "can_revert")
);
},
});
}
}

Some files were not shown because too many files have changed in this diff Show More