diff --git a/forms_pro/api/team.py b/forms_pro/api/team.py index 86a5c9d..574795b 100644 --- a/forms_pro/api/team.py +++ b/forms_pro/api/team.py @@ -1,8 +1,17 @@ import frappe +from frappe.core.api.user_invitation import invite_by_email +from frappe.core.doctype.docshare.docshare import DocShare +from frappe.core.doctype.user_invitation.user_invitation import UserInvitation +from frappe.share import get_share_name from forms_pro.forms_pro.doctype.fp_team.fp_team import FPTeam, GetTeamMembersResponse -from forms_pro.utils.teams import GetTeamFormsResponseSchema, set_current_team -from forms_pro.utils.teams import get_team_forms as get_team_forms_utils +from forms_pro.utils.teams import ( + GetTeamFormsResponseSchema, + set_current_team, +) +from forms_pro.utils.teams import ( + get_team_forms as get_team_forms_utils, +) @frappe.whitelist() @@ -36,6 +45,9 @@ def get_team_members(team_id: str) -> list[GetTeamMembersResponse]: throw=True, ) + # Clear cache so we read fresh DocShare data (e.g. after toggle_can_edit_team) + frappe.clear_document_cache("FP Team", team_id) + team: FPTeam = frappe.get_doc("FP Team", team_id) members = team.team_members @@ -77,3 +89,143 @@ def switch_team(team_id: str) -> None: raise frappe.PermissionError("You do not have permission to switch to this team") set_current_team(team_id, frappe.session.user) + + +@frappe.whitelist(methods=["POST"]) +def invite_team_members(team_id: str, emails: list[str]) -> None: + """ + Invite team members to a team + """ + + if not frappe.has_permission( + doctype="FP Team", + ptype="write", + doc=team_id, + user=frappe.session.user, + ): + raise frappe.PermissionError( + "You do not have write permission on this team; write access is required to invite members" + ) + + emails_str = ", ".join(emails) + + invite_by_email( + emails=emails_str, + roles=["Forms Pro User"], + redirect_to_path=f"/api/v2/method/forms_pro.api.team.add_member_to_team_via_invitation?team_id={team_id}", + app_name="forms_pro", + ) + + +@frappe.whitelist() +def add_member_to_team_via_invitation(team_id: str, invite_id: str | None = None) -> None: + """ + Add a member to a team when an invitation is accepted. + Accepts invite_id from query param (URL may send it as 'id'). + """ + invite_id = invite_id or frappe.form_dict.get("id") + if not invite_id: + raise frappe.PermissionError("Invitation id is required") + + invite: UserInvitation = frappe.get_doc("User Invitation", invite_id) + + if invite.status != "Accepted": + raise frappe.PermissionError("Invitation not accepted") + + if not frappe.has_permission( + doctype="FP Team", + ptype="read", + doc=team_id, + user=invite.invited_by, + ): + raise frappe.PermissionError("You do not have permission to add a member to this team") + + if not frappe.db.exists("User", invite.email): + raise frappe.PermissionError("User not found") + + team: FPTeam = frappe.get_doc("FP Team", team_id) + + if team.is_team_member(invite.email): + raise frappe.DuplicateEntryError("User is already a member of the team") + + team.add_to_team(invite.email) + team.save(ignore_permissions=True) + set_current_team(team_id, invite.email) + + frappe.local.response["type"] = "redirect" + frappe.local.response["location"] = "/forms" + + +@frappe.whitelist(methods=["POST"]) +def toggle_can_edit_team(team_id: str, member_email: str) -> None: + """ + Toggle the can_edit_team permission for a team member + """ + + if not frappe.has_permission( + doctype="FP Team", + ptype="write", + doc=team_id, + user=frappe.session.user, + ): + raise frappe.PermissionError( + "You do not have permission to toggle the can_edit_team permission for this team member" + ) + + team: FPTeam = frappe.get_doc("FP Team", team_id) + if team.owner == member_email: + raise frappe.PermissionError( + "The team owner always retains full permissions and cannot have edit access toggled" + ) + + share_name = get_share_name(doctype="FP Team", name=team_id, user=member_email, everyone=0) + if not share_name: + raise frappe.PermissionError( + "You do not have permission to toggle the can_edit_team permission for this team member" + ) + + share: DocShare = frappe.get_doc("DocShare", share_name) + share.write = not share.write + share.share = not share.share + share.save(ignore_permissions=True) + + +@frappe.whitelist(methods=["POST"]) +def save(team_id: str, fields: dict) -> None: + """ + Update team fields. Only fields present in the dict are updated. + """ + frappe.has_permission( + doctype="FP Team", + ptype="write", + doc=team_id, + user=frappe.session.user, + throw=True, + ) + + ALLOWED_SAVE_FIELDS = ["team_name", "logo"] + + team: FPTeam = frappe.get_doc("FP Team", team_id) + for key, value in fields.items(): + if key not in ALLOWED_SAVE_FIELDS: + frappe.throw(f"Field '{key}' is not allowed") + setattr(team, key, value) + team.save() + + +@frappe.whitelist(methods=["POST"]) +def remove_member_from_team(team_id: str, member_email: str) -> None: + """ + Remove a member from a team + """ + + if not frappe.has_permission( + doctype="FP Team", + ptype="write", + doc=team_id, + user=frappe.session.user, + ): + raise frappe.PermissionError("You do not have permission to remove a member from this team") + + team: FPTeam = frappe.get_doc("FP Team", team_id) + team.remove_from_team(member_email) diff --git a/forms_pro/forms_pro/doctype/fp_team/fp_team.py b/forms_pro/forms_pro/doctype/fp_team/fp_team.py index 13d1655..ac31f00 100644 --- a/forms_pro/forms_pro/doctype/fp_team/fp_team.py +++ b/forms_pro/forms_pro/doctype/fp_team/fp_team.py @@ -3,7 +3,8 @@ import frappe from frappe.model.document import Document -from frappe.share import add_docshare +from frappe.share import add_docshare, get_share_name +from frappe.share import remove as remove_docshare from pydantic import BaseModel, EmailStr from forms_pro.api.user import get_user @@ -14,6 +15,8 @@ class GetTeamMembersResponse(BaseModel): full_name: str user_image: str | None email: EmailStr + can_edit_team: bool + is_owner: bool class FPTeam(Document): @@ -48,6 +51,9 @@ def team_members(self) -> list[GetTeamMembersResponse]: for member in self.users: _user = get_user(member.user) _user["email"] = member.user + share_name = get_share_name(doctype="FP Team", name=self.name, user=member.user, everyone=0) + _user["can_edit_team"] = frappe.db.get_value("DocShare", share_name, "write") + _user["is_owner"] = self.owner == member.user members.append(GetTeamMembersResponse.model_validate(_user).model_dump()) return members @@ -76,7 +82,7 @@ def add_to_team(self, user: str) -> None: Args: user: The user email address """ - if user == "Administrator": + if user == "Administrator" or user == "Guest": return if self.is_team_member(user): @@ -95,3 +101,26 @@ def add_to_team(self, user: str) -> None: share=1, flags={"ignore_share_permission": True}, ) + + def remove_from_team(self, user: str) -> None: + """ + Remove a user from the team + + Args: + user: The user email address + """ + + if user == self.owner: + frappe.throw( + frappe._("Cannot remove the owner from the team"), + frappe.ValidationError, + ) + + self.users = [member for member in self.users if member.user != user] + self.save() + remove_docshare( + doctype="FP Team", + name=self.name, + user=user, + flags={"ignore_permissions": True}, + ) diff --git a/forms_pro/hooks.py b/forms_pro/hooks.py index 1b36b08..420b49b 100644 --- a/forms_pro/hooks.py +++ b/forms_pro/hooks.py @@ -139,7 +139,10 @@ doc_events = { "User": { "on_update": "forms_pro.overrides.roles.handle_forms_pro_role_change", - } + }, + "User Invitation": { + "after_insert": "forms_pro.overrides.invitations.after_insert", + }, } # Scheduled Tasks @@ -239,6 +242,12 @@ # "Logging DocType Name": 30 # days to retain logs # } +user_invitation = { + "allowed_roles": { + "Forms Pro User": ["Forms Pro User"], + }, + "after_accept": ["forms_pro.overrides.invitations.after_accept"], +} website_route_rules = [ {"from_route": "/forms/", "to_route": "forms"}, diff --git a/forms_pro/overrides/invitations.py b/forms_pro/overrides/invitations.py new file mode 100644 index 0000000..8b174e5 --- /dev/null +++ b/forms_pro/overrides/invitations.py @@ -0,0 +1,56 @@ +from urllib.parse import parse_qs, urlparse + +import frappe +from frappe.model.document import Document + +from forms_pro.utils.teams import set_current_team + + +def after_accept(invitation: Document, user: Document, user_inserted: bool) -> None: + """ + Called by Frappe after a User Invitation is accepted. + Adds the invited user to the team they were invited to and updates the + in-memory redirect path so the browser lands on /forms (not the API endpoint). + """ + parsed = urlparse(invitation.redirect_to_path) + qs = parse_qs(parsed.query) + team_id = qs.get("team_id", [None])[0] + + if not team_id or not frappe.db.exists("FP Team", team_id): + return + + from forms_pro.forms_pro.doctype.fp_team.fp_team import FPTeam + + team: FPTeam = frappe.get_doc("FP Team", team_id) + if not team.is_team_member(user.name): + team.add_to_team(user.name) + team.save(ignore_permissions=True) + + set_current_team(team_id, user.name) + + # Update the in-memory path so _accept_invitation redirects the browser to + # /forms instead of the API endpoint URL (which breaks when URL-embedded as + # a redirect_to query param during the password-reset flow). + invitation.redirect_to_path = "/forms" + + +def after_insert(doc: Document, method: str) -> None: + """ + After an invitation is inserted, add the user to the team + """ + if doc.app_name != "forms_pro": + return + + role_names = [r.role for r in doc.roles] if doc.roles else [] + if role_names != ["Forms Pro User"]: + return + + parsed = urlparse(doc.redirect_to_path) + qs = parse_qs(parsed.query) + team_id = qs.get("team_id", [None])[0] + if not team_id: + return + + # Set the redirect path to add the member to the team (invite_id so API receives it) + doc.redirect_to_path = f"/api/v2/method/forms_pro.api.team.add_member_to_team_via_invitation?team_id={team_id}&invite_id={doc.name}" + doc.save(ignore_permissions=True) diff --git a/forms_pro/overrides/roles.py b/forms_pro/overrides/roles.py index fd73088..b0d0a10 100644 --- a/forms_pro/overrides/roles.py +++ b/forms_pro/overrides/roles.py @@ -32,6 +32,13 @@ def handle_forms_pro_role_change(doc, method) -> None: if not has_forms_pro_role_before_save and has_forms_pro_role_after_save: if len(get_user_teams(user.name)) > 0: return + # If the user was invited via Forms Pro, the invitation redirect will + # add them to the correct team — don't create a spurious default team. + if frappe.db.exists( + "User Invitation", + {"email": user.name, "app_name": "forms_pro", "status": ["in", ["Pending", "Accepted"]]}, + ): + return create_default_team_for_user(user) diff --git a/forms_pro/tests/test_invitations.py b/forms_pro/tests/test_invitations.py new file mode 100644 index 0000000..cefd52d --- /dev/null +++ b/forms_pro/tests/test_invitations.py @@ -0,0 +1,340 @@ +# Copyright (c) 2025, harsh@buildwithhussain.com and contributors +# For license information, please see license.txt + +from unittest.mock import patch + +import frappe +from faker import Faker +from frappe.tests import IntegrationTestCase + +from forms_pro.api.team import add_member_to_team_via_invitation, invite_team_members +from forms_pro.overrides.invitations import after_accept, after_insert +from forms_pro.roles import FORMS_PRO_ROLE + + +class _StubRole: + def __init__(self, role: str): + self.role = role + + +class _StubInvitationDoc: + """Minimal doc-like object for testing after_insert skip logic without DB.""" + + def __init__(self, app_name: str, redirect_to_path: str, roles: list): + self.app_name = app_name + self.redirect_to_path = redirect_to_path + self.roles = roles + self.name = "STUB-INV-001" + + def save(self, ignore_permissions: bool = False): + pass + + +class TestTeamInvitations(IntegrationTestCase): + """Tests for team invitation flow: invite_team_members, after_insert hook, add_member_to_team_via_invitation.""" + + def setUp(self): + super().setUp() + self.sendmail_patcher = patch("frappe.sendmail") + self.sendmail_patcher.start() + self.fake = Faker() + self.teams_created = [] + self.users_created = [] + frappe.set_user("Administrator") + + def tearDown(self): + patcher = getattr(self, "sendmail_patcher", None) + if patcher is not None: + patcher.stop() + frappe.set_user("Administrator") + for team_id in self.teams_created: + if frappe.db.exists("FP Team", team_id): + frappe.delete_doc("FP Team", team_id, force=True, ignore_permissions=True) + for email in self.users_created: + if frappe.db.exists("User", email): + frappe.delete_doc("User", email, force=True, ignore_permissions=True) + frappe.db.delete("User Invitation", {"app_name": "forms_pro"}) + frappe.db.commit() + super().tearDown() + + def _create_user(self, email: str | None = None, with_forms_pro_role: bool = True) -> str: + email = email or self.fake.email() + if frappe.db.exists("User", email): + return email + user = frappe.get_doc( + { + "doctype": "User", + "email": email, + "first_name": self.fake.first_name(), + "last_name": self.fake.last_name(), + } + ) + user.insert(ignore_permissions=True) + if with_forms_pro_role: + user.append_roles(FORMS_PRO_ROLE) + user.save(ignore_permissions=True) + self.users_created.append(email) + return email + + def _create_team(self, owner: str, team_name: str | None = None) -> str: + frappe.set_user(owner) + team = frappe.get_doc( + { + "doctype": "FP Team", + "team_name": team_name or f"{self.fake.word()} Team", + } + ) + team.insert() + self.teams_created.append(team.name) + frappe.set_user("Administrator") + return team.name + + def _make_accepted_invitation(self, invitee_email: str, owner: str) -> object: + inv_doc = frappe.get_doc( + { + "doctype": "User Invitation", + "email": invitee_email, + "app_name": "forms_pro", + "redirect_to_path": "/forms", + "roles": [{"role": FORMS_PRO_ROLE}], + "invited_by": owner, + } + ) + inv_doc.insert(ignore_permissions=True) + inv_doc.db_set("status", "Accepted") + inv_doc.db_set("user", invitee_email) + frappe.db.commit() + return inv_doc + + def _get_team_memberships(self, email: str) -> list: + return frappe.get_all("FP Team Member", filters={"user": email}) + + # --- invite_team_members --- + + def test_invite_requires_permission(self): + """User without read permission on team cannot invite members.""" + owner = self._create_user() + other = self._create_user(with_forms_pro_role=False) + team_id = self._create_team(owner) + + frappe.set_user(other) + with self.assertRaises(frappe.PermissionError) as ctx: + invite_team_members(team_id=team_id, emails=[self.fake.email()]) + self.assertIn("permission", str(ctx.exception).lower()) + + def test_invite_creates_invitation_with_redirect(self): + """Inviting creates a User Invitation whose redirect contains team_id and invite_id.""" + owner = self._create_user() + invitee_email = self.fake.email() + team_id = self._create_team(owner) + + frappe.set_user(owner) + invite_team_members(team_id=team_id, emails=[invitee_email]) + frappe.db.commit() + + invitations = frappe.get_all( + "User Invitation", + filters={"email": invitee_email, "app_name": "forms_pro"}, + fields=["name", "redirect_to_path"], + ) + self.assertEqual(len(invitations), 1) + inv = invitations[0] + self.assertIn(team_id, inv.redirect_to_path) + self.assertIn("add_member_to_team_via_invitation", inv.redirect_to_path) + self.assertIn(f"invite_id={inv.name}", inv.redirect_to_path) + + # --- after_insert hook --- + + def test_after_insert_adds_invite_id_to_redirect(self): + """after_insert hook rewrites redirect_to_path to include invite_id.""" + team_id = self._create_team(self._create_user()) + doc = frappe.get_doc( + { + "doctype": "User Invitation", + "email": self.fake.email(), + "app_name": "forms_pro", + "redirect_to_path": f"/api/v2/method/forms_pro.api.team.add_member_to_team_via_invitation?team_id={team_id}", + "roles": [{"role": FORMS_PRO_ROLE}], + } + ) + doc.insert(ignore_permissions=True) + doc.reload() + self.assertIn(team_id, doc.redirect_to_path) + self.assertIn(f"invite_id={doc.name}", doc.redirect_to_path) + self.assertIn("add_member_to_team_via_invitation", doc.redirect_to_path) + frappe.delete_doc("User Invitation", doc.name, force=True) + + def test_after_insert_skips_non_forms_pro_app(self): + """after_insert does not modify redirect when app_name is not forms_pro.""" + original_path = "/some/path?team_id=abc" + doc = _StubInvitationDoc( + app_name="other_app", redirect_to_path=original_path, roles=[_StubRole(FORMS_PRO_ROLE)] + ) + after_insert(doc, "after_insert") + self.assertEqual(doc.redirect_to_path, original_path) + + def test_after_insert_skips_wrong_roles(self): + """after_insert does not modify redirect when roles do not include Forms Pro User.""" + team_id = self._create_team(self._create_user()) + original_path = f"/api/v2/method/some.method?team_id={team_id}" + doc = _StubInvitationDoc( + app_name="forms_pro", redirect_to_path=original_path, roles=[_StubRole("System Manager")] + ) + after_insert(doc, "after_insert") + self.assertEqual(doc.redirect_to_path, original_path) + + # --- add_member_to_team_via_invitation --- + + def test_add_member_raises_when_invitation_not_accepted(self): + """Raises PermissionError when invitation status is not Accepted.""" + owner = self._create_user() + team_id = self._create_team(owner) + inv_doc = frappe.get_doc( + { + "doctype": "User Invitation", + "email": self.fake.email(), + "app_name": "forms_pro", + "redirect_to_path": "/forms", + "roles": [{"role": FORMS_PRO_ROLE}], + "invited_by": owner, + "status": "Pending", + } + ) + inv_doc.insert(ignore_permissions=True) + frappe.db.commit() + + with self.assertRaises(frappe.PermissionError) as ctx: + add_member_to_team_via_invitation(team_id=team_id, invite_id=inv_doc.name) + self.assertIn("Invitation not accepted", str(ctx.exception)) + + def test_add_member_raises_when_user_not_found(self): + """Raises PermissionError when the invited email has no User record.""" + owner = self._create_user() + team_id = self._create_team(owner) + inv_doc = frappe.get_doc( + { + "doctype": "User Invitation", + "email": self.fake.email(), + "app_name": "forms_pro", + "redirect_to_path": "/forms", + "roles": [{"role": FORMS_PRO_ROLE}], + "invited_by": owner, + } + ) + inv_doc.insert(ignore_permissions=True) + inv_doc.db_set("status", "Accepted") + frappe.db.commit() + + with self.assertRaises(frappe.PermissionError) as ctx: + add_member_to_team_via_invitation(team_id=team_id, invite_id=inv_doc.name) + self.assertIn("User not found", str(ctx.exception)) + + def test_add_member_adds_user_to_team_and_redirects(self): + """Successfully adds user to team and sets redirect response to /forms.""" + owner = self._create_user() + invitee_email = self._create_user(with_forms_pro_role=False) + team_id = self._create_team(owner) + inv_doc = self._make_accepted_invitation(invitee_email, owner) + + add_member_to_team_via_invitation(team_id=team_id, invite_id=inv_doc.name) + + team = frappe.get_doc("FP Team", team_id) + self.assertIn(invitee_email, [row.user for row in team.users]) + self.assertEqual(frappe.local.response.get("type"), "redirect") + self.assertEqual(frappe.local.response.get("location"), "/forms") + + # --- after_accept hook --- + + def test_after_accept_adds_user_to_inviting_team(self): + """after_accept adds the invited user to the team and updates redirect to /forms.""" + owner = self._create_user() + invitee_email = self._create_user(with_forms_pro_role=False) + team_id = self._create_team(owner) + + inv_doc = frappe.get_doc( + { + "doctype": "User Invitation", + "email": invitee_email, + "app_name": "forms_pro", + "redirect_to_path": f"/api/v2/method/forms_pro.api.team.add_member_to_team_via_invitation?team_id={team_id}&invite_id=TEST-001", + "roles": [{"role": FORMS_PRO_ROLE}], + "invited_by": owner, + } + ) + inv_doc.insert(ignore_permissions=True) + frappe.db.commit() + + invitee_user = frappe.get_doc("User", invitee_email) + after_accept(invitation=inv_doc, user=invitee_user, user_inserted=True) + + team = frappe.get_doc("FP Team", team_id) + self.assertIn(invitee_email, [row.user for row in team.users]) + self.assertEqual(inv_doc.redirect_to_path, "/forms") + frappe.delete_doc("User Invitation", inv_doc.name, force=True) + + def test_after_accept_skips_when_no_team_id(self): + """after_accept is a no-op when redirect_to_path has no team_id.""" + owner = self._create_user() + invitee_email = self._create_user(with_forms_pro_role=False) + inv_doc = frappe.get_doc( + { + "doctype": "User Invitation", + "email": invitee_email, + "app_name": "forms_pro", + "redirect_to_path": "/forms", + "roles": [{"role": FORMS_PRO_ROLE}], + "invited_by": owner, + } + ) + inv_doc.insert(ignore_permissions=True) + frappe.db.commit() + + invitee_user = frappe.get_doc("User", invitee_email) + after_accept(invitation=inv_doc, user=invitee_user, user_inserted=True) + + # redirect_to_path unchanged; no team membership created + self.assertEqual(inv_doc.redirect_to_path, "/forms") + self.assertEqual(self._get_team_memberships(invitee_email), []) + frappe.delete_doc("User Invitation", inv_doc.name, force=True) + + # --- default team creation guard (bug fix) --- + + def test_no_default_team_created_for_invited_user(self): + """ + When a user receives the Forms Pro role but has a pending invitation, + the role-change hook must NOT create a default team — the invitation + redirect will place them in the correct inviting team instead. + """ + invitee_email = self._create_user(with_forms_pro_role=False) + owner = self._create_user() + team_id = self._create_team(owner) + + # Simulate a pending invitation (exists before the user accepts) + frappe.get_doc( + { + "doctype": "User Invitation", + "email": invitee_email, + "app_name": "forms_pro", + "redirect_to_path": f"/api/v2/method/forms_pro.api.team.add_member_to_team_via_invitation?team_id={team_id}", + "roles": [{"role": FORMS_PRO_ROLE}], + "invited_by": owner, + "status": "Pending", + } + ).insert(ignore_permissions=True) + frappe.db.commit() + + memberships_before = self._get_team_memberships(invitee_email) + + # Assigning the Forms Pro role is what Frappe does on invitation acceptance; + # the hook must skip default team creation here. + user = frappe.get_doc("User", invitee_email) + user.append_roles(FORMS_PRO_ROLE) + user.save(ignore_permissions=True) + frappe.db.commit() + + memberships_after = self._get_team_memberships(invitee_email) + self.assertEqual( + len(memberships_before), + len(memberships_after), + "A default team must not be created for an invited user", + ) diff --git a/frontend/package.json b/frontend/package.json index 138ff02..06cde99 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -17,7 +17,7 @@ "dayjs": "^1.11.19", "feather-icons": "^4.29.2", "frappe-ui": "^0.1.262", - "lucide-vue-next": "^0.543.0", + "lucide-vue-next": "^0.575.0", "pinia": "^3.0.3", "socket.io-client": "^4.7.2", "vue": "^3.5.13", diff --git a/frontend/src/components/team/InviteMemberDialog.vue b/frontend/src/components/team/InviteMemberDialog.vue new file mode 100644 index 0000000..ea4acb2 --- /dev/null +++ b/frontend/src/components/team/InviteMemberDialog.vue @@ -0,0 +1,118 @@ + + diff --git a/frontend/src/components/team/ManageTeamHeader.vue b/frontend/src/components/team/ManageTeamHeader.vue new file mode 100644 index 0000000..71e5733 --- /dev/null +++ b/frontend/src/components/team/ManageTeamHeader.vue @@ -0,0 +1,72 @@ + + diff --git a/frontend/src/components/team/RemoveMemberDialog.vue b/frontend/src/components/team/RemoveMemberDialog.vue new file mode 100644 index 0000000..522f0dc --- /dev/null +++ b/frontend/src/components/team/RemoveMemberDialog.vue @@ -0,0 +1,54 @@ + + diff --git a/frontend/src/components/team/TeamMemberList.vue b/frontend/src/components/team/TeamMemberList.vue new file mode 100644 index 0000000..8704544 --- /dev/null +++ b/frontend/src/components/team/TeamMemberList.vue @@ -0,0 +1,95 @@ + + diff --git a/frontend/src/components/team/TeamSwitcher.vue b/frontend/src/components/team/TeamSwitcher.vue index f4d3750..216ec5e 100644 --- a/frontend/src/components/team/TeamSwitcher.vue +++ b/frontend/src/components/team/TeamSwitcher.vue @@ -48,7 +48,7 @@ const groupOptions = computed(() => { diff --git a/frontend/src/pages/Dashboard.vue b/frontend/src/pages/Dashboard.vue deleted file mode 100644 index ba6655c..0000000 --- a/frontend/src/pages/Dashboard.vue +++ /dev/null @@ -1,167 +0,0 @@ - - - diff --git a/frontend/src/pages/home/Dashboard.vue b/frontend/src/pages/home/Dashboard.vue new file mode 100644 index 0000000..0806b75 --- /dev/null +++ b/frontend/src/pages/home/Dashboard.vue @@ -0,0 +1,159 @@ + + + diff --git a/frontend/src/pages/home/Home.vue b/frontend/src/pages/home/Home.vue new file mode 100644 index 0000000..64f72e7 --- /dev/null +++ b/frontend/src/pages/home/Home.vue @@ -0,0 +1,14 @@ + + diff --git a/frontend/src/pages/home/sidebarItems.ts b/frontend/src/pages/home/sidebarItems.ts new file mode 100644 index 0000000..291f1b1 --- /dev/null +++ b/frontend/src/pages/home/sidebarItems.ts @@ -0,0 +1,38 @@ +import { Files, UsersRound } from "lucide-vue-next"; +import { computed } from "vue"; +import { useRoute } from "vue-router"; +import type { SidebarProps } from "frappe-ui"; + +type SidebarSectionProps = NonNullable< + SidebarProps["sections"] +> extends (infer T)[] + ? T + : never; + +export function useSidebarItems() { + const route = useRoute(); + const isActive = (path: string) => route.path === path; + + return computed((): SidebarSectionProps[] => [ + { + items: [ + { + label: "All Forms", + to: "/", + icon: Files, + isActive: isActive("/"), + }, + ], + }, + { + items: [ + { + label: "Team", + to: "/team", + icon: UsersRound, + isActive: isActive("/team"), + }, + ], + }, + ]); +} diff --git a/frontend/src/pages/manage/ManageForm.vue b/frontend/src/pages/manage/ManageForm.vue index 671f5e2..d871112 100644 --- a/frontend/src/pages/manage/ManageForm.vue +++ b/frontend/src/pages/manage/ManageForm.vue @@ -69,7 +69,7 @@ const breadcrumbItems = computed(() => {