Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,5 @@ jspm_packages/
.aider*

/forms_pro/public/frontend
/forms_pro/www/frontend.html
/forms_pro/www/frontend.html
/forms_pro/public/node_modules
Empty file.
8 changes: 8 additions & 0 deletions forms_pro/forms_pro/doctype/fp_team/fp_team.js
Original file line number Diff line number Diff line change
@@ -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) {

// },
// });
67 changes: 67 additions & 0 deletions forms_pro/forms_pro/doctype/fp_team/fp_team.json
Original file line number Diff line number Diff line change
@@ -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
}
23 changes: 23 additions & 0 deletions forms_pro/forms_pro/doctype/fp_team/fp_team.py
Original file line number Diff line number Diff line change
@@ -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
20 changes: 20 additions & 0 deletions forms_pro/forms_pro/doctype/fp_team/test_fp_team.py
Original file line number Diff line number Diff line change
@@ -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
Empty file.
8 changes: 8 additions & 0 deletions forms_pro/forms_pro/doctype/fp_team_member/fp_team_member.js
Original file line number Diff line number Diff line change
@@ -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) {

// },
// });
32 changes: 32 additions & 0 deletions forms_pro/forms_pro/doctype/fp_team_member/fp_team_member.json
Original file line number Diff line number Diff line change
@@ -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": []
}
23 changes: 23 additions & 0 deletions forms_pro/forms_pro/doctype/fp_team_member/fp_team_member.py
Original file line number Diff line number Diff line change
@@ -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
20 changes: 20 additions & 0 deletions forms_pro/forms_pro/doctype/fp_team_member/test_fp_team_member.py
Original file line number Diff line number Diff line change
@@ -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
12 changes: 5 additions & 7 deletions forms_pro/hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
# ---------------
Expand Down
42 changes: 42 additions & 0 deletions forms_pro/overrides/roles.py
Original file line number Diff line number Diff line change
@@ -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)
5 changes: 5 additions & 0 deletions forms_pro/roles.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
"""
This module contains the roles for Forms Pro
"""

FORMS_PRO_ROLE = "Forms Pro User"
36 changes: 36 additions & 0 deletions forms_pro/tests/test_roles.py
Original file line number Diff line number Diff line change
@@ -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")
21 changes: 21 additions & 0 deletions forms_pro/utils/teams.py
Original file line number Diff line number Diff line change
@@ -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 []
2 changes: 0 additions & 2 deletions frontend/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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']
Expand Down
8 changes: 4 additions & 4 deletions frontend/postcss.config.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};
6 changes: 4 additions & 2 deletions frontend/src/components/dashboard/FormPreviewCard.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<script setup>
import { Badge } from "frappe-ui";
import { FileText } from "lucide-vue-next";
import { computed } from "vue";

const props = defineProps({
Expand All @@ -18,15 +19,16 @@ const formattedDate = computed(() => {
@click="$router.push({ name: 'Edit Form', params: { id: props.form.name } })"
class="flex flex-col gap-2 border rounded p-4 hover:border-gray-400 transition-all duration-300 cursor-pointer"
>
<div class="flex flex-col gap-3">
<div class="flex flex-col gap-3 text-ink-gray-6">
<FileText class="w-5 h-5" />
<h3 class="text-lg font-medium">{{ props.form.title }}</h3>
<div class="flex gap-2 items-center">
<Badge
class="w-fit"
:label="props.form.is_published ? 'Published' : 'Draft'"
:theme="props.form.is_published ? 'green' : 'gray'"
/>
<p class="text-sm text-gray-500">{{ formattedDate }}</p>
<p class="text-sm text-ink-gray-4">{{ formattedDate }}</p>
</div>
</div>
</div>
Expand Down
Loading