diff --git a/src/metabase/api/collection.clj b/src/metabase/api/collection.clj index 461f7e3ff3282dd42339891ddd1178cfd356e47c..dc9ac4bde625fd4098e0a003a253964eed3ae9e6 100644 --- a/src/metabase/api/collection.clj +++ b/src/metabase/api/collection.clj @@ -267,13 +267,14 @@ [:pinned-state {:optional true} [:maybe (into [:enum] (map keyword) valid-pinned-state-values)]] ;; when specified, only return results of this type. [:models {:optional true} [:maybe [:set (into [:enum] (map keyword) valid-model-param-values)]]] - [:sort-info {:optional true} [:maybe [:tuple - (into [:enum {:error/message "sort-columns"}] - (map normalize-sort-choice) - valid-sort-columns) - (into [:enum {:error/message "sort-direction"}] - (map normalize-sort-choice) - valid-sort-directions)]]]]) + [:sort-info {:optional true} [:maybe [:map + [:sort-column (into [:enum {:error/message "sort-columns"}] + (map normalize-sort-choice) + valid-sort-columns)] + [:sort-direction (into [:enum {:error/message "sort-direction"}] + (map normalize-sort-choice) + valid-sort-directions)] + [:official-collections-first? {:optional true} :boolean]]]]]) (defmulti ^:private collection-children-query "Query that will fetch the 'children' of a `collection`, for different types of objects. Possible options are listed @@ -815,19 +816,25 @@ (for [model [:card :metric :dataset :dashboard :snippet :pulse :collection :timeline]] (:select (collection-children-query model {:id 1 :location "/"} nil))))) +(defn- official-collections-first-sort-clause [{:keys [official-collections-first?]}] + (when official-collections-first? + [[[:case [:= :authority_level "official"] 0 :else 1]] :asc])) + +(def ^:private normal-collections-first-sort-clause + [[[:case + [:= :collection_type nil] 0 + [:= :collection_type collection/trash-collection-type] 1 + :else 2]] + :asc]) (defn children-sort-clause "Given the client side sort-info, return sort clause to effect this. `db-type` is necessary due to complications from treatment of nulls in the different app db types." [sort-info db-type] - ;; always put "Metabase Analytics" last - (into [[[[:case [:= :authority_level "official"] 0 :else 1]] :asc] - [[[:case - [:= :collection_type nil] 0 - [:= :collection_type collection/trash-collection-type] 1 - :else 2]] :asc]] - (case sort-info - nil [[:%lower.name :asc]] + (->> (into [(official-collections-first-sort-clause sort-info) + normal-collections-first-sort-clause] + (case ((juxt :sort-column :sort-direction) sort-info) + [nil nil] [[:%lower.name :asc]] [:name :asc] [[:%lower.name :asc]] [:name :desc] [[:%lower.name :desc]] [:last-edited-at :asc] [(if (= db-type :mysql) @@ -835,13 +842,12 @@ [:last_edit_timestamp :nulls-last]) [:last_edit_timestamp :asc] [:%lower.name :asc]] - [:last-edited-at :desc] (remove nil? - [(case db-type - :mysql [:%isnull.last_edit_timestamp] - :postgres [:last_edit_timestamp :desc-nulls-last] - :h2 nil) - [:last_edit_timestamp :desc] - [:%lower.name :asc]]) + [:last-edited-at :desc] [(case db-type + :mysql [:%isnull.last_edit_timestamp] + :postgres [:last_edit_timestamp :desc-nulls-last] + :h2 nil) + [:last_edit_timestamp :desc] + [:%lower.name :asc]] [:last-edited-by :asc] [(if (= db-type :mysql) [:%isnull.last_edit_last_name] [:last_edit_last_name :nulls-last]) @@ -851,20 +857,21 @@ [:last_edit_first_name :nulls-last]) [:last_edit_first_name :asc] [:%lower.name :asc]] - [:last-edited-by :desc] (remove nil? - [(case db-type - :mysql [:%isnull.last_edit_last_name] - :postgres [:last_edit_last_name :desc-nulls-last] - :h2 nil) - [:last_edit_last_name :desc] - (case db-type - :mysql [:%isnull.last_edit_first_name] - :postgres [:last_edit_last_name :desc-nulls-last] - :h2 nil) - [:last_edit_first_name :desc] - [:%lower.name :asc]]) + [:last-edited-by :desc] [(case db-type + :mysql [:%isnull.last_edit_last_name] + :postgres [:last_edit_last_name :desc-nulls-last] + :h2 nil) + [:last_edit_last_name :desc] + (case db-type + :mysql [:%isnull.last_edit_first_name] + :postgres [:last_edit_last_name :desc-nulls-last] + :h2 nil) + [:last_edit_first_name :desc] + [:%lower.name :asc]] [:model :asc] [[:model_ranking :asc] [:%lower.name :asc]] - [:model :desc] [[:model_ranking :desc] [:%lower.name :asc]]))) + [:model :desc] [[:model_ranking :desc] [:%lower.name :asc]])) + (remove nil?) + (into []))) (defn- collection-children* [collection models {:keys [sort-info] :as options}] @@ -976,21 +983,27 @@ * `pinned_state` - when `is_pinned`, return pinned objects only. when `is_not_pinned`, return non pinned objects only. when `all`, return everything. By default returns everything" - [id models archived pinned_state sort_column sort_direction] - {id ms/PositiveInt - models [:maybe Models] - archived [:maybe ms/BooleanValue] - pinned_state [:maybe (into [:enum] valid-pinned-state-values)] - sort_column [:maybe (into [:enum] valid-sort-columns)] - sort_direction [:maybe (into [:enum] valid-sort-directions)]} + [id models archived pinned_state sort_column sort_direction official_collections_first] + {id ms/PositiveInt + models [:maybe Models] + archived [:maybe ms/BooleanValue] + pinned_state [:maybe (into [:enum] valid-pinned-state-values)] + sort_column [:maybe (into [:enum] valid-sort-columns)] + sort_direction [:maybe (into [:enum] valid-sort-directions)] + official_collections_first [:maybe ms/MaybeBooleanValue]} (let [model-kwds (set (map keyword (u/one-or-many models))) collection (api/read-check Collection id)] (u/prog1 (collection-children collection {:models model-kwds :archived? (or archived (:archived collection) (collection/is-trash? collection)) :pinned-state (keyword pinned_state) - :sort-info [(or (some-> sort_column normalize-sort-choice) :name) - (or (some-> sort_direction normalize-sort-choice) :asc)]}) + :sort-info {:sort-column (or (some-> sort_column normalize-sort-choice) :name) + :sort-direction (or (some-> sort_direction normalize-sort-choice) :asc) + ;; default to sorting official collections first, except for the trash. + :official-collections-first? (if (and (nil? official_collections_first) + (not (collection/is-trash? collection))) + true + (boolean official_collections_first))}}) (events/publish-event! :event/collection-read {:object collection :user-id api/*current-user-id*})))) @@ -1033,13 +1046,14 @@ By default, this will show the 'normal' Collections namespace; to view a different Collections namespace, such as `snippets`, you can pass the `?namespace=` parameter." - [models archived namespace pinned_state sort_column sort_direction] - {models [:maybe Models] - archived [:maybe ms/BooleanValue] - namespace [:maybe ms/NonBlankString] - pinned_state [:maybe (into [:enum] valid-pinned-state-values)] - sort_column [:maybe (into [:enum] valid-sort-columns)] - sort_direction [:maybe (into [:enum] valid-sort-directions)]} + [models archived namespace pinned_state sort_column sort_direction official_collections_first] + {models [:maybe Models] + archived [:maybe ms/BooleanValue] + namespace [:maybe ms/NonBlankString] + pinned_state [:maybe (into [:enum] valid-pinned-state-values)] + sort_column [:maybe (into [:enum] valid-sort-columns)] + sort_direction [:maybe (into [:enum] valid-sort-directions)] + official_collections_first [:maybe ms/MaybeBooleanValue]} ;; Return collection contents, including Collections that have an effective location of being in the Root ;; Collection for the Current User. (let [root-collection (assoc collection/root-collection :namespace namespace) @@ -1050,8 +1064,11 @@ {:archived? (boolean archived) :models model-kwds :pinned-state (keyword pinned_state) - :sort-info [(or (some-> sort_column normalize-sort-choice) :name) - (or (some-> sort_direction normalize-sort-choice) :asc)]}))) + :sort-info {:sort-column (or (some-> sort_column normalize-sort-choice) :name) + :sort-direction (or (some-> sort_direction normalize-sort-choice) :asc) + ;; default to sorting official collections first, but provide the option not to + :official-collections-first? (or (nil? official_collections_first) + (boolean official_collections_first))}}))) ;;; ----------------------------------------- Creating/Editing a Collection ------------------------------------------ diff --git a/test/metabase/api/collection_test.clj b/test/metabase/api/collection_test.clj index 840a1f4520cfacbba644853dc8bca1e7d5898510..0efbdcc068a863cb5f04ca57ac876b072f869604 100644 --- a/test/metabase/api/collection_test.clj +++ b/test/metabase/api/collection_test.clj @@ -1111,7 +1111,7 @@ [:= :collection_type collection/trash-collection-type] 1 :else 2]] :asc] [:%lower.name :asc]] - (api.collection/children-sort-clause nil app-db))))) + (api.collection/children-sort-clause {:official-collections-first? true} app-db))))) (testing "Sorting by last-edited-at" (is (= [[[[:case [:= :authority_level "official"] 0 :else 1]] :asc] [[[:case @@ -1121,7 +1121,9 @@ [:%isnull.last_edit_timestamp] [:last_edit_timestamp :asc] [:%lower.name :asc]] - (api.collection/children-sort-clause [:last-edited-at :asc] :mysql))) + (api.collection/children-sort-clause {:sort-column :last-edited-at + :sort-direction :asc + :official-collections-first? true} :mysql))) (is (= [[[[:case [:= :authority_level "official"] 0 :else 1]] :asc] [[[:case [:= :collection_type nil] 0 @@ -1130,7 +1132,9 @@ [:last_edit_timestamp :nulls-last] [:last_edit_timestamp :asc] [:%lower.name :asc]] - (api.collection/children-sort-clause [:last-edited-at :asc] :postgres)))) + (api.collection/children-sort-clause {:sort-column :last-edited-at + :sort-direction :asc + :official-collections-first? true} :postgres)))) (testing "Sorting by last-edited-by" (is (= [[[[:case [:= :authority_level "official"] 0 :else 1]] :asc] [[[:case @@ -1142,7 +1146,9 @@ [:last_edit_first_name :nulls-last] [:last_edit_first_name :asc] [:%lower.name :asc]] - (api.collection/children-sort-clause [:last-edited-by :asc] :postgres))) + (api.collection/children-sort-clause {:sort-column :last-edited-by + :sort-direction :asc + :official-collections-first? true} :postgres))) (is (= [[[[:case [:= :authority_level "official"] 0 :else 1]] :asc] [[[:case [:= :collection_type nil] 0 @@ -1153,7 +1159,9 @@ [:%isnull.last_edit_first_name] [:last_edit_first_name :asc] [:%lower.name :asc]] - (api.collection/children-sort-clause [:last-edited-by :asc] :mysql)))) + (api.collection/children-sort-clause {:sort-column :last-edited-by + :sort-direction :asc + :official-collections-first? true} :mysql)))) (testing "Sorting by model" (is (= [[[[:case [:= :authority_level "official"] 0 :else 1]] :asc] [[[:case @@ -1162,7 +1170,9 @@ :else 2]] :asc] [:model_ranking :asc] [:%lower.name :asc]] - (api.collection/children-sort-clause [:model :asc] :postgres))) + (api.collection/children-sort-clause {:sort-column :model + :sort-direction :asc + :official-collections-first? true} :postgres))) (is (= [[[[:case [:= :authority_level "official"] 0 :else 1]] :asc] [[[:case [:= :collection_type nil] 0 @@ -1170,7 +1180,9 @@ :else 2]] :asc] [:model_ranking :desc] [:%lower.name :asc]] - (api.collection/children-sort-clause [:model :desc] :mysql))))) + (api.collection/children-sort-clause {:sort-column :model + :sort-direction :desc + :official-collections-first? true} :mysql))))) (deftest snippet-collection-items-test (testing "GET /api/collection/:id/items"