diff --git a/lib/sanbase/queries/menu/menu.ex b/lib/sanbase/queries/menu/menu.ex index f562aa5e8..51e733260 100644 --- a/lib/sanbase/queries/menu/menu.ex +++ b/lib/sanbase/queries/menu/menu.ex @@ -28,25 +28,32 @@ defmodule Sanbase.Queries.Menu do :parent_id, :root_parent_id ]) - |> validate_required([:name]) end def create(attrs \\ %{}) do %Menu{} |> changeset(attrs) + |> validate_required([:name]) + |> validate_length(:name, min: 1, max: 256) end def update(menu, attrs) do menu |> changeset(attrs) - |> Repo.update() end - def delete(menu) do - Repo.delete(menu) + def get(id) do + base_query() + |> where([m], m.id == ^id) + end + + def get_root_parent_id(id) do + base_query() + |> where([m], m.id == ^id) + |> select([m], m.root_parent_id) end - def get(id) do - from(m in Menu, where: m.id == ^id) + defp base_query() do + __MODULE__ end end diff --git a/lib/sanbase/queries/menu/menu_item.ex b/lib/sanbase/queries/menu/menu_item.ex index 4292fedec..3c5bf0211 100644 --- a/lib/sanbase/queries/menu/menu_item.ex +++ b/lib/sanbase/queries/menu/menu_item.ex @@ -9,11 +9,11 @@ defmodule Sanbase.Queries.Menu.MenuItem do alias Sanbase.Queries.Dashboard schema "menu_items" do - belongs_to(:menu, Menu, foreign_key: :menu_id) - belongs_to(:root_menu, Menu, foreign_key: :root_menu_id) + belongs_to(:parent, Menu, foreign_key: :parent_id) belongs_to(:query, Query, foreign_key: :query_id) belongs_to(:dashboard, Dashboard, foreign_key: :dashboard_id) + belongs_to(:menu, Menu, foreign_key: :menu_id) field(:position, :integer) @@ -30,27 +30,39 @@ defmodule Sanbase.Queries.Menu.MenuItem do ]) end + @doc ~s""" + Get the next position for a menu item inside a specific menu + (it can be either sub-menu or a root menu) + """ + def get_next_position(menu_id) do + base_query() + |> where([m], m.menu_id == ^menu_id) + |> select([m], coalesce(max(m.position), 0) + 1) + end + + @doc ~s""" + Get the next position for a menu item inside a specific menu + (it can be either sub-menu or a root menu) + """ + def inc_all_positions_after(menu_id, position) do + base_query() + |> where([m], m.menu_id == ^menu_id and m.position >= ^position) + |> update([m], inc: [position: +1]) + end + def create(attrs \\ %{}) do %__MODULE__{} |> changeset(attrs) - |> Repo.insert() end def update(menu, attrs) do menu |> changeset(attrs) - |> Repo.update() - end - - def delete(menu) do - Repo.delete(menu) end - def get(id) do - Repo.get(Menu, id) - end + # Private functions - def list do - Repo.all(Menu) + defp base_query() do + from(m in __MODULE__) end end diff --git a/lib/sanbase/queries/menus.ex b/lib/sanbase/queries/menus.ex index 1ee0e0d6f..9b1313973 100644 --- a/lib/sanbase/queries/menus.ex +++ b/lib/sanbase/queries/menus.ex @@ -2,24 +2,89 @@ defmodule Sanbase.Menus do alias Sanbase.Queries.Menu alias Sanbase.Queries.Menu.MenuItem - def create_menu(args) do - Menu.create(args) - |> Sanbase.Repo.insert() + import Sanbase.Utils.ErrorHandling, only: [changeset_errors_string: 1] + + @doc ~s""" + TODO + """ + def create_menu(params) do + Ecto.Multi.new() + |> Ecto.Multi.run(:get_root_parent_id, fn _repo, _changes -> + case Map.get(params, :parent_id) do + # If there is no parent_id then there is no root_parent_id as well + nil -> + {:ok, nil} + + # The root_parent_id of the sub-menu is the same as the root_parent_id + # of the parent menu. Root menus don't have a root_parent_id pointing to themselves + # so in such case we put the parent_id as a root_parent_id (i.e. this new menu + # is a first-level child) + parent_id when is_integer(parent_id) -> + {:ok, root_parent_id} = get_root_parent_id(parent_id) + + {:ok, root_parent_id || parent_id} + end + end) + |> Ecto.Multi.run(:create_menu, fn _repo, %{get_root_parent_id: root_parent_id} -> + params = Map.merge(params, %{root_parent_id: root_parent_id}) + query = Menu.create(params) + Sanbase.Repo.insert(query) + end) + |> Sanbase.Repo.transaction() + |> process_transaction_result(:create_menu) end + @doc ~s""" + TODO + """ def add_menu_item(menu_id, item_args) do - with {:ok, root_parent_id} <- get_root_parent_id(menu_id), - {:ok, position} <- MenuItem.get_next_position(root_parent_id) do - MenuItem.create(%{menu_id: menu_id, root_menu_id: root_parent_id} ++ item_args) - |> Sanbase.Repo.insert() - end + Ecto.Multi.new() + |> Ecto.Multi.run(:get_root_parent_id, fn _repo, _changes -> + get_root_parent_id(menu_id) + end) + |> Ecto.Multi.run(:get_and_adjust_position, fn _repo, %{get_root_parent_id: root_parent_id} -> + case Map.get(item_args, :position) do + nil -> + # If `position` is not specified, add it at the end by getting the last position + 1 + {:ok, get_next_position(menu_id)} - MenuItem.create(%{menu_id: menu_id} ++ item_args) + position when is_integer(position) -> + # If `position` is specified, bump all the positions bigger than it by 1 + {:ok, {nil, _}} = inc_all_positions_after(menu_id, position) + {:ok, position} + end + end) + |> Ecto.Multi.run( + :create_menu_item, + fn _repo, %{get_root_parent_id: _, get_and_adjust_position: _} = map -> + params = Map.merge(item_args, map) + query = MenuItem.create(params) + Sanbase.Repo.insert(query) + end + ) end - defp get_root_parent_id(menu_id) do - query = Menu.get(menu_id) |> select(:root_parent_id) + defp get_next_position(menu_id) do + query = MenuItem.get_next_position(menu_id) + {:ok, Sanbase.Repo.one(query)} + end - {:ok, root_parent_id} = Sanbase.Repo.one(query) + defp inc_all_positions_after(menu_id, position) do + query = MenuItem.inc_all_positions_after(menu_id, position) + {:ok, Sanbase.Repo.update_all(query)} end + + defp get_root_parent_id(menu_id) when is_integer(menu_id) do + query = Menu.get_root_parent_id(menu_id) + {:ok, Sanbase.Repo.one(query)} + end + + defp process_transaction_result({:ok, map}, ok_field), + do: {:ok, map[ok_field]} + + defp process_transaction_result({:error, _, %Ecto.Changeset{} = changeset, _}, _ok_field), + do: {:error, changeset_errors_string(changeset)} + + defp process_transaction_result({:error, _, error, _}, _ok_field), + do: {:error, error} end diff --git a/lib/sanbase_web/graphql/schema/queries/menu_queries.ex b/lib/sanbase_web/graphql/schema/queries/menu_queries.ex index 2c10fe54c..c21204cd8 100644 --- a/lib/sanbase_web/graphql/schema/queries/menu_queries.ex +++ b/lib/sanbase_web/graphql/schema/queries/menu_queries.ex @@ -52,59 +52,5 @@ defmodule SanbaseWeb.Graphql.Schema.MenuQueriesQueries do resolve(&MenuResolver.add_menu_item/3) end - - field :create_menu_question, :string do - arg(:menu_uuid, non_null(:string)) - - arg(:params, :menu_question_params_input_object) - - middleware(JWTAuth) - - resolve(&MenuResolver.create_question/3) - end - - field :update_menu_question, :menu do - arg(:question_uuid, non_null(:string)) - arg(:params, :menu_question_params_input_object) - - middleware(JWTAuth) - - resolve(&MenuResolver.update_question/3) - end - - field :delete_menu_question, :menu do - arg(:question_uuid, non_null(:string)) - - middleware(JWTAuth) - - resolve(&MenuResolver.delete_question/3) - end - - field :create_menu_answer, :menu_answer do - arg(:question_uuid, non_null(:string)) - arg(:params, :menu_answer_params_input_object) - - middleware(JWTAuth) - - resolve(&MenuResolver.create_answer/3) - end - - field :update_menu_answer, :menu_answer do - arg(:answer_uuid, non_null(:string)) - arg(:params, :menu_answer_params_input_object) - - middleware(JWTAuth) - - resolve(&MenuResolver.update_answer/3) - end - - field :delete_menu_answer, :menu_answer do - arg(:answer_uuid, non_null(:string)) - arg(:params, :menu_answer_params_input_object) - - middleware(JWTAuth) - - resolve(&MenuResolver.delete_answer/3) - end end end diff --git a/priv/repo/migrations/20231110093800_create_menus_table.exs b/priv/repo/migrations/20231110093800_create_menus_table.exs index c8801a97a..4062877b9 100644 --- a/priv/repo/migrations/20231110093800_create_menus_table.exs +++ b/priv/repo/migrations/20231110093800_create_menus_table.exs @@ -9,8 +9,6 @@ defmodule Sanbase.Repo.Migrations.CreatesMenusTable do add(:parent_id, references(:menus, on_delete: :delete_all)) add(:root_parent_id, references(:menus, on_delete: :delete_all)) - add(:position, :integer) - add(:user_id, references(:users, on_delete: :delete_all)) add(:is_admin_controlled, :boolean, default: false) @@ -25,22 +23,18 @@ defmodule Sanbase.Repo.Migrations.CreatesMenusTable do create(index(:menus, [:user_id])) create table(:menu_items) do - add(:menu_id, references(:menus, on_delete: :delete_all)) - add(:root_menu_id, references(:menus, on_delete: :delete_all)) + add(:parent_id, references(:menus, on_delete: :delete_all)) + # add(:root_parent_id, references(:menus, on_delete: :delete_all)) add(:query_id, references(:queries, on_delete: :delete_all)) add(:dashboard_id, references(:dashboards, on_delete: :delete_all)) + add(:menu_id, references(:menus, on_delete: :delete_all)) add(:position, :integer) timestamps() end - # When searching for all menu items of a menu, the `where` clause will - # find these items by the root_menu_id, hence the index. The - # menu_id is used only to build the hierarchy after that. - create(index(:menu_items, [:root_menu_id])) - fk_check = """ (CASE WHEN query_id IS NULL THEN 0 ELSE 1 END) + (CASE WHEN dashboard_id IS NULL THEN 0 ELSE 1 END) = 1 diff --git a/priv/repo/structure.sql b/priv/repo/structure.sql index 5f43872a8..1ac7007df 100644 --- a/priv/repo/structure.sql +++ b/priv/repo/structure.sql @@ -2011,10 +2011,10 @@ ALTER SEQUENCE public.market_segments_id_seq OWNED BY public.market_segments.id; CREATE TABLE public.menu_items ( id bigint NOT NULL, - menu_id bigint, - root_menu_id bigint, + parent_id bigint, query_id bigint, dashboard_id bigint, + menu_id bigint, "position" integer, inserted_at timestamp without time zone NOT NULL, updated_at timestamp without time zone NOT NULL, @@ -2059,7 +2059,6 @@ CREATE TABLE public.menus ( description character varying(255), parent_id bigint, root_parent_id bigint, - "position" integer, user_id bigint, is_admin_controlled boolean DEFAULT false, inserted_at timestamp without time zone NOT NULL, @@ -6732,13 +6731,6 @@ CREATE UNIQUE INDEX list_items_user_list_id_project_id_index ON public.list_item CREATE UNIQUE INDEX market_segments_name_index ON public.market_segments USING btree (name); --- --- Name: menu_items_root_menu_id_index; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX menu_items_root_menu_id_index ON public.menu_items USING btree (root_menu_id); - - -- -- Name: menus_root_parent_id_index; Type: INDEX; Schema: public; Owner: - -- @@ -7906,19 +7898,19 @@ ALTER TABLE ONLY public.menu_items -- --- Name: menu_items menu_items_query_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- Name: menu_items menu_items_parent_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- ALTER TABLE ONLY public.menu_items - ADD CONSTRAINT menu_items_query_id_fkey FOREIGN KEY (query_id) REFERENCES public.queries(id) ON DELETE CASCADE; + ADD CONSTRAINT menu_items_parent_id_fkey FOREIGN KEY (parent_id) REFERENCES public.menus(id) ON DELETE CASCADE; -- --- Name: menu_items menu_items_root_menu_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- Name: menu_items menu_items_query_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- ALTER TABLE ONLY public.menu_items - ADD CONSTRAINT menu_items_root_menu_id_fkey FOREIGN KEY (root_menu_id) REFERENCES public.menus(id) ON DELETE CASCADE; + ADD CONSTRAINT menu_items_query_id_fkey FOREIGN KEY (query_id) REFERENCES public.queries(id) ON DELETE CASCADE; --