diff --git a/.gitignore b/.gitignore index 8a36b70..5d81179 100644 --- a/.gitignore +++ b/.gitignore @@ -55,4 +55,5 @@ jspm_packages/ .aider* /forms_pro/public/frontend -/forms_pro/www/frontend.html \ No newline at end of file +/forms_pro/www/frontend.html +/forms_pro/public/node_modules \ No newline at end of file diff --git a/forms_pro/forms_pro/doctype/fp_team/__init__.py b/forms_pro/forms_pro/doctype/fp_team/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/forms_pro/forms_pro/doctype/fp_team/fp_team.js b/forms_pro/forms_pro/doctype/fp_team/fp_team.js new file mode 100644 index 0000000..c7893ff --- /dev/null +++ b/forms_pro/forms_pro/doctype/fp_team/fp_team.js @@ -0,0 +1,8 @@ +// Copyright (c) 2025, harsh@buildwithhussain.com and contributors +// For license information, please see license.txt + +// frappe.ui.form.on("FP Team", { +// refresh(frm) { + +// }, +// }); diff --git a/forms_pro/forms_pro/doctype/fp_team/fp_team.json b/forms_pro/forms_pro/doctype/fp_team/fp_team.json new file mode 100644 index 0000000..5027140 --- /dev/null +++ b/forms_pro/forms_pro/doctype/fp_team/fp_team.json @@ -0,0 +1,67 @@ +{ + "actions": [], + "allow_rename": 1, + "autoname": "hash", + "creation": "2025-11-19 19:33:34.271135", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "team_name", + "users" + ], + "fields": [ + { + "fieldname": "team_name", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Team Name", + "reqd": 1 + }, + { + "fieldname": "users", + "fieldtype": "Table MultiSelect", + "label": "Users", + "options": "FP Team Member" + } + ], + "grid_page_length": 50, + "index_web_pages_for_search": 1, + "links": [], + "modified": "2025-11-19 19:43:50.515720", + "modified_by": "Administrator", + "module": "Forms Pro", + "name": "FP Team", + "naming_rule": "Random", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Forms Pro User", + "share": 1, + "write": 1 + } + ], + "row_format": "Dynamic", + "sort_field": "creation", + "sort_order": "DESC", + "states": [], + "title_field": "team_name", + "track_changes": 1 +} diff --git a/forms_pro/forms_pro/doctype/fp_team/fp_team.py b/forms_pro/forms_pro/doctype/fp_team/fp_team.py new file mode 100644 index 0000000..b346ad6 --- /dev/null +++ b/forms_pro/forms_pro/doctype/fp_team/fp_team.py @@ -0,0 +1,23 @@ +# Copyright (c) 2025, harsh@buildwithhussain.com and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class FPTeam(Document): + # begin: auto-generated types + # This code is auto-generated. Do not modify anything in this block. + + from typing import TYPE_CHECKING + + if TYPE_CHECKING: + from frappe.types import DF + + from forms_pro.forms_pro.doctype.fp_team_member.fp_team_member import FPTeamMember + + team_name: DF.Data + users: DF.TableMultiSelect[FPTeamMember] + # end: auto-generated types + + pass diff --git a/forms_pro/forms_pro/doctype/fp_team/test_fp_team.py b/forms_pro/forms_pro/doctype/fp_team/test_fp_team.py new file mode 100644 index 0000000..e353069 --- /dev/null +++ b/forms_pro/forms_pro/doctype/fp_team/test_fp_team.py @@ -0,0 +1,20 @@ +# Copyright (c) 2025, harsh@buildwithhussain.com and Contributors +# See license.txt + +# import frappe +from frappe.tests import IntegrationTestCase + +# On IntegrationTestCase, the doctype test records and all +# link-field test record dependencies are recursively loaded +# Use these module variables to add/remove to/from that list +EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"] +IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"] + + +class IntegrationTestFPTeam(IntegrationTestCase): + """ + Integration tests for FPTeam. + Use this class for testing interactions between multiple components. + """ + + pass diff --git a/forms_pro/forms_pro/doctype/fp_team_member/__init__.py b/forms_pro/forms_pro/doctype/fp_team_member/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/forms_pro/forms_pro/doctype/fp_team_member/fp_team_member.js b/forms_pro/forms_pro/doctype/fp_team_member/fp_team_member.js new file mode 100644 index 0000000..670c41c --- /dev/null +++ b/forms_pro/forms_pro/doctype/fp_team_member/fp_team_member.js @@ -0,0 +1,8 @@ +// Copyright (c) 2025, harsh@buildwithhussain.com and contributors +// For license information, please see license.txt + +// frappe.ui.form.on("FP Team Member", { +// refresh(frm) { + +// }, +// }); diff --git a/forms_pro/forms_pro/doctype/fp_team_member/fp_team_member.json b/forms_pro/forms_pro/doctype/fp_team_member/fp_team_member.json new file mode 100644 index 0000000..b71f103 --- /dev/null +++ b/forms_pro/forms_pro/doctype/fp_team_member/fp_team_member.json @@ -0,0 +1,32 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2025-11-19 19:40:08.423952", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "user" + ], + "fields": [ + { + "fieldname": "user", + "fieldtype": "Link", + "label": "User", + "options": "User" + } + ], + "grid_page_length": 50, + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2025-11-19 19:41:00.110515", + "modified_by": "Administrator", + "module": "Forms Pro", + "name": "FP Team Member", + "owner": "Administrator", + "permissions": [], + "row_format": "Dynamic", + "sort_field": "creation", + "sort_order": "DESC", + "states": [] +} diff --git a/forms_pro/forms_pro/doctype/fp_team_member/fp_team_member.py b/forms_pro/forms_pro/doctype/fp_team_member/fp_team_member.py new file mode 100644 index 0000000..1754f22 --- /dev/null +++ b/forms_pro/forms_pro/doctype/fp_team_member/fp_team_member.py @@ -0,0 +1,23 @@ +# Copyright (c) 2025, harsh@buildwithhussain.com and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class FPTeamMember(Document): + # begin: auto-generated types + # This code is auto-generated. Do not modify anything in this block. + + from typing import TYPE_CHECKING + + if TYPE_CHECKING: + from frappe.types import DF + + parent: DF.Data + parentfield: DF.Data + parenttype: DF.Data + user: DF.Link | None + # end: auto-generated types + + pass diff --git a/forms_pro/forms_pro/doctype/fp_team_member/test_fp_team_member.py b/forms_pro/forms_pro/doctype/fp_team_member/test_fp_team_member.py new file mode 100644 index 0000000..938123e --- /dev/null +++ b/forms_pro/forms_pro/doctype/fp_team_member/test_fp_team_member.py @@ -0,0 +1,20 @@ +# Copyright (c) 2025, harsh@buildwithhussain.com and Contributors +# See license.txt + +# import frappe +from frappe.tests import IntegrationTestCase + +# On IntegrationTestCase, the doctype test records and all +# link-field test record dependencies are recursively loaded +# Use these module variables to add/remove to/from that list +EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"] +IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"] + + +class IntegrationTestFPTeamMember(IntegrationTestCase): + """ + Integration tests for FPTeamMember. + Use this class for testing interactions between multiple components. + """ + + pass diff --git a/forms_pro/hooks.py b/forms_pro/hooks.py index 7b4f840..8b88771 100644 --- a/forms_pro/hooks.py +++ b/forms_pro/hooks.py @@ -136,13 +136,11 @@ # --------------- # Hook on document methods and events -# doc_events = { -# "*": { -# "on_update": "method", -# "on_cancel": "method", -# "on_trash": "method" -# } -# } +doc_events = { + "User": { + "on_update": "forms_pro.overrides.roles.handle_forms_pro_role_change", + } +} # Scheduled Tasks # --------------- diff --git a/forms_pro/overrides/roles.py b/forms_pro/overrides/roles.py new file mode 100644 index 0000000..b2b09af --- /dev/null +++ b/forms_pro/overrides/roles.py @@ -0,0 +1,42 @@ +import frappe +from frappe.core.doctype.user.user import User + +from forms_pro.roles import FORMS_PRO_ROLE +from forms_pro.utils.teams import get_user_teams + + +def handle_forms_pro_role_change(doc, method) -> None: + user: User = frappe.get_doc("User", doc.name) + user.on_update() + + if user.has_value_changed("roles"): + try: + doc_before_save = user.get_doc_before_save() + except AttributeError: + doc_before_save = None + + roles_before_save = doc_before_save.get("roles") if doc_before_save else [] + roles_after_save = user.get("roles") + + has_forms_pro_role_before_save = any(role.role == FORMS_PRO_ROLE for role in roles_before_save) + has_forms_pro_role_after_save = any(role.role == FORMS_PRO_ROLE for role in roles_after_save) + + if not has_forms_pro_role_before_save and has_forms_pro_role_after_save: + if len(get_user_teams(user.name)) > 0: + return + create_default_team_for_user(user) + + +def create_default_team_for_user(user: User): + from forms_pro.forms_pro.doctype.fp_team.fp_team import FPTeam + + team: FPTeam = frappe.new_doc("FP Team") + team.team_name = f"{user.first_name}'s Team" + team.insert(ignore_permissions=True) + team.append( + "users", + { + "user": user.name, + }, + ) + team.save(ignore_permissions=True) diff --git a/forms_pro/roles.py b/forms_pro/roles.py new file mode 100644 index 0000000..9f0d90c --- /dev/null +++ b/forms_pro/roles.py @@ -0,0 +1,5 @@ +""" +This module contains the roles for Forms Pro +""" + +FORMS_PRO_ROLE = "Forms Pro User" diff --git a/forms_pro/tests/test_roles.py b/forms_pro/tests/test_roles.py new file mode 100644 index 0000000..5e0c8b0 --- /dev/null +++ b/forms_pro/tests/test_roles.py @@ -0,0 +1,36 @@ +import frappe +from faker import Faker +from frappe.core.doctype.user.user import User +from frappe.tests import IntegrationTestCase + +from forms_pro.roles import FORMS_PRO_ROLE +from forms_pro.utils.teams import get_user_teams + + +class TestRoles(IntegrationTestCase): + def setUp(self): + super().setUp() + + def test_roles(self): + fake = Faker() + user: User = frappe.get_doc( + { + "doctype": "User", + "email": fake.email(), + "first_name": fake.first_name(), + "last_name": fake.last_name(), + } + ) + user.insert() + roles = frappe.get_roles(user.name) + self.assertNotIn(FORMS_PRO_ROLE, roles) + self.assertEqual(len(get_user_teams(user.name)), 0) + + user.append_roles(FORMS_PRO_ROLE) + user.save() + roles = frappe.get_roles(user.name) + self.assertIn(FORMS_PRO_ROLE, roles) + + team = get_user_teams(user.name) + self.assertEqual(len(team), 1) + self.assertEqual(team[0].team_name, f"{user.first_name}'s Team") diff --git a/forms_pro/utils/teams.py b/forms_pro/utils/teams.py new file mode 100644 index 0000000..69484a7 --- /dev/null +++ b/forms_pro/utils/teams.py @@ -0,0 +1,21 @@ +import frappe + + +def get_user_teams(user: str = frappe.session.user): + """ + Get all Forms Pro teams for a user + """ + + FP_TEAM = frappe.qb.DocType("FP Team") + FP_TEAM_MEMBER = frappe.qb.DocType("FP Team Member") + + query = ( + frappe.qb.from_(FP_TEAM) + .join(FP_TEAM_MEMBER) + .on(FP_TEAM.name == FP_TEAM_MEMBER.parent) + .where(FP_TEAM_MEMBER.user == user) + .select(FP_TEAM.team_name, FP_TEAM.name) + ) + teams = query.run(as_dict=True) + + return teams or [] diff --git a/frontend/components.d.ts b/frontend/components.d.ts index 65399b9..e46e47a 100644 --- a/frontend/components.d.ts +++ b/frontend/components.d.ts @@ -19,8 +19,6 @@ declare module 'vue' { FormHeader: typeof import('./src/components/submission/FormHeader.vue')['default'] FormPreviewCard: typeof import('./src/components/dashboard/FormPreviewCard.vue')['default'] FormRenderer: typeof import('./src/components/submission/FormRenderer.vue')['default'] - Icon: typeof import('./src/components/Icon.vue')['default'] - Logo: typeof import('./src/components/Logo.vue')['default'] RenderField: typeof import('./src/components/RenderField.vue')['default'] RouterLink: typeof import('vue-router')['RouterLink'] RouterView: typeof import('vue-router')['RouterView'] diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js index 7b75c83..49c0612 100644 --- a/frontend/postcss.config.js +++ b/frontend/postcss.config.js @@ -1,6 +1,6 @@ export default { - plugins: { - tailwindcss: {}, - autoprefixer: {}, - }, + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, }; diff --git a/frontend/src/components/dashboard/FormPreviewCard.vue b/frontend/src/components/dashboard/FormPreviewCard.vue index c95ca81..0b2e2d9 100644 --- a/frontend/src/components/dashboard/FormPreviewCard.vue +++ b/frontend/src/components/dashboard/FormPreviewCard.vue @@ -1,5 +1,6 @@