From 9bd701161a97f22b92a52f308f64ceec25ada21b Mon Sep 17 00:00:00 2001 From: Harsh Tandiya Date: Thu, 25 Dec 2025 01:40:34 +0530 Subject: [PATCH 01/21] feat: add `User Forms` module and update modules list --- forms_pro/modules.txt | 3 ++- forms_pro/user_forms/__init__.py | 0 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 forms_pro/user_forms/__init__.py diff --git a/forms_pro/modules.txt b/forms_pro/modules.txt index ea7829e..f0d9322 100644 --- a/forms_pro/modules.txt +++ b/forms_pro/modules.txt @@ -1 +1,2 @@ -Forms Pro \ No newline at end of file +Forms Pro +User Forms \ No newline at end of file diff --git a/forms_pro/user_forms/__init__.py b/forms_pro/user_forms/__init__.py new file mode 100644 index 0000000..e69de29 From 81c605eb252c36ec6e6ffe137e596c56a87c7d0a Mon Sep 17 00:00:00 2001 From: Harsh Tandiya Date: Thu, 25 Dec 2025 02:36:52 +0530 Subject: [PATCH 02/21] feat: enhance form generation with error handling and permissions management - Added error handling in create_form to log and throw exceptions. - Updated DocType module name to "User Forms". - Implemented permissions management for DocType and Form documents using DocShare. - Added tests to verify DocShare creation with correct permissions for both DocType and Form. --- forms_pro/utils/form_generator.py | 67 ++++++++++++++++++++++++-- forms_pro/utils/test_form_generator.py | 58 +++++++++++++++++++++- 2 files changed, 120 insertions(+), 5 deletions(-) diff --git a/forms_pro/utils/form_generator.py b/forms_pro/utils/form_generator.py index 28faeac..d831b2e 100644 --- a/forms_pro/utils/form_generator.py +++ b/forms_pro/utils/form_generator.py @@ -1,4 +1,5 @@ import frappe +from frappe.share import add_docshare FORMS_PRO_ROLE = "Forms Pro User" @@ -21,11 +22,17 @@ def create_form_with_doctype(team_id: str, doctype: str): @frappe.whitelist() def create_form(team_id: str): roles = frappe.get_roles(frappe.session.user) + print(roles) if FORMS_PRO_ROLE not in roles: frappe.throw("You are not authorized to create a form") - form_generator = FormGenerator(team_id=team_id) - form_generator.generate() + try: + form_generator = FormGenerator(team_id=team_id) + form_generator.generate() + except Exception as e: + frappe.log_error(f"Error creating form: {e}") + frappe.throw(f"Error creating form: {e}") + print(frappe.get_traceback()) return { "doctype": form_generator.doctype.name, @@ -47,6 +54,7 @@ def __init__( def generate(self) -> None: self._initialize_doctype() self._initialize_form_document() + frappe.clear_cache() def _initialize_doctype(self) -> None: if self.doctype: @@ -54,13 +62,53 @@ def _initialize_doctype(self) -> None: placeholder_doctype = frappe.new_doc("DocType") placeholder_doctype.name = self._generate_doctype_name() - placeholder_doctype.module = "Forms Pro" + placeholder_doctype.module = "User Forms" placeholder_doctype.custom = True placeholder_doctype.track_changes = True placeholder_doctype.track_views = True placeholder_doctype.index_web_pages_for_search = False + placeholder_doctype.is_submittable = 0 + placeholder_doctype.istable = 0 + placeholder_doctype.editable_grid = 0 + placeholder_doctype.issingle = 0 + placeholder_doctype.is_tree = 0 + placeholder_doctype.is_virtual = 0 + + # Add permissions + placeholder_doctype.append( + "permissions", + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1, + "submit": 0, + }, + ) + + # Add fields + placeholder_doctype.append("fields", {"fieldtype": "Section Break"}) + placeholder_doctype.insert(ignore_permissions=True) + add_docshare( + "DocType", + placeholder_doctype.name, + read=1, + write=1, + share=1, + submit=0, + flags={ + "ignore_share_permission": True, + }, + ) + self.doctype = placeholder_doctype # self.set_placeholder_doctype_fields() @@ -86,7 +134,20 @@ def _initialize_form_document(self) -> None: form_document.linked_doctype = self.doctype.name form_document.title = "Untitled Form" form_document.linked_team_id = self.team_id + form_document.document_type = "System" form_document.insert(ignore_permissions=True) + + add_docshare( + "Form", + form_document.name, + read=1, + write=1, + share=1, + submit=0, + flags={ + "ignore_share_permission": True, + }, + ) self.form_document = form_document def _generate_doctype_name(self) -> str: diff --git a/forms_pro/utils/test_form_generator.py b/forms_pro/utils/test_form_generator.py index 3ed680a..2998165 100644 --- a/forms_pro/utils/test_form_generator.py +++ b/forms_pro/utils/test_form_generator.py @@ -74,7 +74,7 @@ def test_generate_creates_doctype_when_none_provided(self): # Assertions self.assertIsNotNone(form_generator.doctype) self.assertTrue(form_generator.doctype.name.startswith("formspro_")) - self.assertEqual(form_generator.doctype.module, "Forms Pro") + self.assertEqual(form_generator.doctype.module, "User Forms") self.assertTrue(form_generator.doctype.custom) self.assertTrue(form_generator.doctype.track_changes) self.assertTrue(form_generator.doctype.track_views) @@ -133,7 +133,7 @@ def test_generate_complete_flow(self): # Verify DocType properties self.assertTrue(form_generator.doctype.name.startswith("formspro_")) - self.assertEqual(form_generator.doctype.module, "Forms Pro") + self.assertEqual(form_generator.doctype.module, "User Forms") self.assertTrue(form_generator.doctype.custom) def test_generate_with_existing_doctype_complete_flow(self): @@ -157,3 +157,57 @@ def test_generate_with_existing_doctype_complete_flow(self): # Verify Form document exists in database self.assertTrue(frappe.db.exists("Form", form_generator.form_document.name)) + + def test_generate_creates_doctype_docshare(self): + """Test that generate() creates DocShare for the DocType with correct permissions""" + frappe.set_user(self.test_user) + form_generator = FormGenerator(team_id=self.test_team) + + # Call generate method + form_generator.generate() + + # Query DocShare for the DocType + docshare = frappe.db.get_value( + "DocShare", + { + "share_doctype": "DocType", + "share_name": form_generator.doctype.name, + "user": self.test_user, + }, + ["read", "write", "share", "submit"], + as_dict=True, + ) + + # Assertions + self.assertIsNotNone(docshare, "DocShare should exist for DocType") + self.assertEqual(docshare.read, 1, "DocShare should have read permission") + self.assertEqual(docshare.write, 1, "DocShare should have write permission") + self.assertEqual(docshare.share, 1, "DocShare should have share permission") + self.assertEqual(docshare.submit, 0, "DocShare should not have submit permission") + + def test_generate_creates_form_docshare(self): + """Test that generate() creates DocShare for the Form document with correct permissions""" + frappe.set_user(self.test_user) + form_generator = FormGenerator(team_id=self.test_team) + + # Call generate method + form_generator.generate() + + # Query DocShare for the Form document + docshare = frappe.db.get_value( + "DocShare", + { + "share_doctype": "Form", + "share_name": form_generator.form_document.name, + "user": self.test_user, + }, + ["read", "write", "share", "submit"], + as_dict=True, + ) + + # Assertions + self.assertIsNotNone(docshare, "DocShare should exist for Form document") + self.assertEqual(docshare.read, 1, "DocShare should have read permission") + self.assertEqual(docshare.write, 1, "DocShare should have write permission") + self.assertEqual(docshare.share, 1, "DocShare should have share permission") + self.assertEqual(docshare.submit, 0, "DocShare should not have submit permission") From bbc1bd2046b119925ef40938c1d6f7b4c5a7c602 Mon Sep 17 00:00:00 2001 From: Harsh Tandiya Date: Thu, 25 Dec 2025 02:44:16 +0530 Subject: [PATCH 03/21] fix: error handling --- forms_pro/utils/form_generator.py | 44 ++++++++++++++----------------- 1 file changed, 20 insertions(+), 24 deletions(-) diff --git a/forms_pro/utils/form_generator.py b/forms_pro/utils/form_generator.py index d831b2e..c0ebb25 100644 --- a/forms_pro/utils/form_generator.py +++ b/forms_pro/utils/form_generator.py @@ -1,4 +1,5 @@ import frappe +from frappe import _ from frappe.share import add_docshare FORMS_PRO_ROLE = "Forms Pro User" @@ -8,10 +9,19 @@ def create_form_with_doctype(team_id: str, doctype: str): roles = frappe.get_roles(frappe.session.user) if FORMS_PRO_ROLE not in roles: - frappe.throw("You are not authorized to create a form") + frappe.throw( + _("You are not authorized to create a form"), + frappe.PermissionError, + ) - form_generator = FormGenerator(team_id=team_id, linked_doctype=doctype) - form_generator.generate() + try: + form_generator = FormGenerator(team_id=team_id, linked_doctype=doctype) + form_generator.generate() + except Exception as e: + frappe.log_error(f"Error creating form with doctype: {doctype} - {e}") + frappe.throw( + _("Error creating form with doctype: {0} - {1}").format(doctype, e), + ) return { "doctype": form_generator.doctype.name, @@ -24,15 +34,19 @@ def create_form(team_id: str): roles = frappe.get_roles(frappe.session.user) print(roles) if FORMS_PRO_ROLE not in roles: - frappe.throw("You are not authorized to create a form") + frappe.throw( + _("You are not authorized to create a form"), + frappe.PermissionError, + ) try: form_generator = FormGenerator(team_id=team_id) form_generator.generate() except Exception as e: frappe.log_error(f"Error creating form: {e}") - frappe.throw(f"Error creating form: {e}") - print(frappe.get_traceback()) + frappe.throw( + _("Error creating form: {0}").format(e), + ) return { "doctype": form_generator.doctype.name, @@ -110,24 +124,6 @@ def _initialize_doctype(self) -> None: ) self.doctype = placeholder_doctype - # self.set_placeholder_doctype_fields() - - # def set_placeholder_doctype_fields(self) -> None: - # fields = [ - # { - # "label": "Is Form Pro DocType", - # "fieldname": "is_forms_pro_doctype", - # "fieldtype": "Check", - # "reqd": 1, - # "read_only": 1, - # "default": 1, - # } - # ] - - # for field in fields: - # self.doctype.append("fields", field) - - # self.doctype.save(ignore_permissions=True) def _initialize_form_document(self) -> None: form_document = frappe.new_doc("Form") From 4c088a61736e26aa3ddef4c864c162ad9ee12292 Mon Sep 17 00:00:00 2001 From: Harsh Tandiya Date: Thu, 25 Dec 2025 02:45:07 +0530 Subject: [PATCH 04/21] refactor: remove debug print statement from create_form function --- forms_pro/utils/form_generator.py | 1 - 1 file changed, 1 deletion(-) diff --git a/forms_pro/utils/form_generator.py b/forms_pro/utils/form_generator.py index c0ebb25..e570a9d 100644 --- a/forms_pro/utils/form_generator.py +++ b/forms_pro/utils/form_generator.py @@ -32,7 +32,6 @@ def create_form_with_doctype(team_id: str, doctype: str): @frappe.whitelist() def create_form(team_id: str): roles = frappe.get_roles(frappe.session.user) - print(roles) if FORMS_PRO_ROLE not in roles: frappe.throw( _("You are not authorized to create a form"), From 3433fb591551ff3f017620417b1a9c6e696855c5 Mon Sep 17 00:00:00 2001 From: Harsh Tandiya Date: Thu, 25 Dec 2025 02:48:19 +0530 Subject: [PATCH 05/21] chore: update Python version in CI workflow to 3.12 --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2ba218c..a8add4a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -46,7 +46,7 @@ jobs: - name: Setup Python uses: actions/setup-python@v4 with: - python-version: '3.10' + python-version: "3.12" - name: Setup Node uses: actions/setup-node@v3 @@ -96,7 +96,7 @@ jobs: bench --site test_site install-app forms_pro bench build env: - CI: 'Yes' + CI: "Yes" - name: Run Tests working-directory: /home/runner/frappe-bench From 5823e4ed0d957247cb4a0ad450d578b2a6bf700a Mon Sep 17 00:00:00 2001 From: Harsh Tandiya Date: Thu, 25 Dec 2025 02:54:07 +0530 Subject: [PATCH 06/21] chore: update Python version in CI workflow to 3.14 --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a8add4a..063c340 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -46,7 +46,7 @@ jobs: - name: Setup Python uses: actions/setup-python@v4 with: - python-version: "3.12" + python-version: "3.14" - name: Setup Node uses: actions/setup-node@v3 From 91b062e52c241e3edcd8319e56d5c9a227963b0c Mon Sep 17 00:00:00 2001 From: Harsh Tandiya Date: Thu, 25 Dec 2025 02:57:01 +0530 Subject: [PATCH 07/21] chore: update Node.js version in CI workflow to 24 --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 063c340..77b67e2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -51,7 +51,7 @@ jobs: - name: Setup Node uses: actions/setup-node@v3 with: - node-version: 18 + node-version: 24 check-latest: true - name: Cache pip From 435b88bcced7abc54e08e41692762ad4a79d32be Mon Sep 17 00:00:00 2001 From: Harsh Tandiya Date: Fri, 26 Dec 2025 17:35:33 +0530 Subject: [PATCH 08/21] chore: add dayjs dependency to frontend --- frontend/package.json | 1 + frontend/yarn.lock | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/frontend/package.json b/frontend/package.json index 818485b..39abbff 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -13,6 +13,7 @@ "dependencies": { "@lottiefiles/dotlottie-vue": "^0.10.4", "@vueuse/core": "^13.9.0", + "dayjs": "^1.11.19", "feather-icons": "^4.29.2", "frappe-ui": "^0.1.214", "lucide-vue-next": "^0.543.0", diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 201310e..17bbd24 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -1491,6 +1491,11 @@ dayjs@^1.11.13: resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.18.tgz#835fa712aac52ab9dec8b1494098774ed7070a11" integrity sha512-zFBQ7WFRvVRhKcWoUh+ZA1g2HVgUbsZm9sbddh8EC5iv93sui8DVVz1Npvz+r6meo9VKfa8NyLWBsQK1VvIKPA== +dayjs@^1.11.19: + version "1.11.19" + resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.19.tgz#15dc98e854bb43917f12021806af897c58ae2938" + integrity sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw== + debug@4, debug@^4.1.1, debug@^4.3.4, debug@^4.3.7, debug@^4.4.0, debug@^4.4.1: version "4.4.1" resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.1.tgz#e5a8bc6cbc4c6cd3e64308b0693a3d4fa550189b" From 96c882b185c0506126c6f093f3dd1a6ec9ac5330 Mon Sep 17 00:00:00 2001 From: Harsh Tandiya Date: Sat, 27 Dec 2025 00:50:01 +0530 Subject: [PATCH 09/21] feat: add date utility functions using dayjs - Introduced a new utility module for date formatting and manipulation. - Added functions for formatting dates, checking if dates are today/past/future, and calculating date differences. - Enhanced user experience with human-readable date formats and relative time displays. --- frontend/src/utils/date.ts | 175 +++++++++++++++++++++++++++++++++++++ 1 file changed, 175 insertions(+) create mode 100644 frontend/src/utils/date.ts diff --git a/frontend/src/utils/date.ts b/frontend/src/utils/date.ts new file mode 100644 index 0000000..85c5677 --- /dev/null +++ b/frontend/src/utils/date.ts @@ -0,0 +1,175 @@ +import dayjs from "dayjs"; +import relativeTime from "dayjs/plugin/relativeTime"; +import isToday from "dayjs/plugin/isToday"; +import isYesterday from "dayjs/plugin/isYesterday"; +import customParseFormat from "dayjs/plugin/customParseFormat"; + +// Extend dayjs with plugins +dayjs.extend(relativeTime); +dayjs.extend(isToday); +dayjs.extend(isYesterday); +dayjs.extend(customParseFormat); + +/** + * Format a date string or Date object to a readable date format + * @param date - Date string, Date object, or dayjs object + * @param format - Optional format string (default: "MMM D, YYYY") + * @returns Formatted date string + */ +export function formatDate( + date: string | Date | dayjs.Dayjs | null | undefined, + format: string = "MMM D, YYYY", +): string { + if (!date) return ""; + return dayjs(date).format(format); +} + +/** + * Format a date string or Date object to include both date and time + * @param date - Date string, Date object, or dayjs object + * @param format - Optional format string (default: "MMM D, YYYY h:mm A") + * @returns Formatted datetime string + */ +export function formatDateTime( + date: string | Date | dayjs.Dayjs | null | undefined, + format: string = "MMM D, YYYY h:mm A", +): string { + if (!date) return ""; + return dayjs(date).format(format); +} + +/** + * Format a date to a human-readable, pretty format + * Shows relative time for recent dates, formatted date for older ones + * @param date - Date string, Date object, or dayjs object + * @returns Pretty formatted date string + */ +export function formatPrettyDate( + date: string | Date | dayjs.Dayjs | null | undefined, +): string { + if (!date) return ""; + + const dayjsDate = dayjs(date); + const now = dayjs(); + const diffInDays = now.diff(dayjsDate, "day"); + + if (dayjsDate.isToday()) { + return "Today"; + } + + if (dayjsDate.isYesterday()) { + return "Yesterday"; + } + + if (diffInDays < 7) { + return dayjsDate.fromNow(); + } + + if (diffInDays < 365) { + return dayjsDate.format("MMM D"); + } + + return dayjsDate.format("MMM D, YYYY"); +} + +/** + * Format just the time portion of a date + * @param date - Date string, Date object, or dayjs object + * @param format - Optional format string (default: "h:mm A") + * @returns Formatted time string + */ +export function formatTime( + date: string | Date | dayjs.Dayjs | null | undefined, + format: string = "h:mm A", +): string { + if (!date) return ""; + return dayjs(date).format(format); +} + +/** + * Get relative time from now (e.g., "2 hours ago", "in 3 days") + * @param date - Date string, Date object, or dayjs object + * @returns Relative time string + */ +export function getRelativeTime( + date: string | Date | dayjs.Dayjs | null | undefined, +): string { + if (!date) return ""; + return dayjs(date).fromNow(); +} + +/** + * Check if a date is today + * @param date - Date string, Date object, or dayjs object + * @returns True if the date is today + */ +export function isDateToday( + date: string | Date | dayjs.Dayjs | null | undefined, +): boolean { + if (!date) return false; + return dayjs(date).isToday(); +} + +/** + * Check if a date is in the past + * @param date - Date string, Date object, or dayjs object + * @returns True if the date is in the past + */ +export function isDatePast( + date: string | Date | dayjs.Dayjs | null | undefined, +): boolean { + if (!date) return false; + return dayjs(date).isBefore(dayjs()); +} + +/** + * Check if a date is in the future + * @param date - Date string, Date object, or dayjs object + * @returns True if the date is in the future + */ +export function isDateFuture( + date: string | Date | dayjs.Dayjs | null | undefined, +): boolean { + if (!date) return false; + return dayjs(date).isAfter(dayjs()); +} + +/** + * Get the difference between two dates in a specified unit + * @param date1 - First date + * @param date2 - Second date (defaults to now) + * @param unit - Unit of difference (e.g., "day", "hour", "minute") + * @returns Difference in the specified unit + */ +export function getDateDiff( + date1: string | Date | dayjs.Dayjs | null | undefined, + date2: string | Date | dayjs.Dayjs | null | undefined = dayjs(), + unit: dayjs.ManipulateType = "day", +): number { + if (!date1) return 0; + return dayjs(date1).diff(dayjs(date2), unit); +} + +/** + * Format date in a short format (e.g., "12/25/2023") + * @param date - Date string, Date object, or dayjs object + * @returns Short formatted date string + */ +export function formatShortDate( + date: string | Date | dayjs.Dayjs | null | undefined, +): string { + if (!date) return ""; + return dayjs(date).format("M/D/YYYY"); +} + +/** + * Format date in ISO format (e.g., "2023-12-25") + * @param date - Date string, Date object, or dayjs object + * @returns ISO formatted date string + */ +export function formatISODate( + date: string | Date | dayjs.Dayjs | null | undefined, +): string { + if (!date) return ""; + return dayjs(date).format("YYYY-MM-DD"); +} From 091ba601b7a9e8688afc8e6c6aa1291242b18375 Mon Sep 17 00:00:00 2001 From: Harsh Tandiya Date: Sun, 28 Dec 2025 20:38:28 +0530 Subject: [PATCH 10/21] feat: add useQueryParam composable for URL query parameter synchronization - Introduced a new composable function to sync a reactive value with a URL query parameter. - Supports default values, validation against a list of valid values, and options for URL update behavior. - Enhances user experience by keeping the application state in sync with the URL. --- frontend/src/composables/useQueryParam.ts | 112 ++++++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 frontend/src/composables/useQueryParam.ts diff --git a/frontend/src/composables/useQueryParam.ts b/frontend/src/composables/useQueryParam.ts new file mode 100644 index 0000000..189b58e --- /dev/null +++ b/frontend/src/composables/useQueryParam.ts @@ -0,0 +1,112 @@ +import { ref, watch, type Ref } from "vue"; +import { useRoute, useRouter } from "vue-router"; + +/** + * Composable for syncing a reactive value with a URL query parameter + * + * @param paramName - The name of the query parameter + * @param defaultValue - Default value if param is not present or invalid + * @param validValues - Optional array of valid values. If provided, only these values will be accepted + * @param options - Optional configuration + * @returns A reactive ref that stays in sync with the URL query parameter + * + * @example + * ```ts + * const selectedTab = useQueryParam('tab', 'description', ['description', 'shared']); + * ``` + */ +export function useQueryParam( + paramName: string, + defaultValue: T, + validValues?: T[], + options?: { + /** + * Whether to update the URL immediately if param is missing (default: true) + */ + setInitialParam?: boolean; + /** + * Whether to use router.replace (true) or router.push (false) (default: true) + */ + replace?: boolean; + }, +): Ref { + const route = useRoute(); + const router = useRouter(); + const { setInitialParam = true, replace = true } = options || {}; + + // Get initial value from URL or use default + const getInitialValue = (): T => { + const paramValue = route.query[paramName]; + + if (!paramValue || typeof paramValue !== "string") { + return defaultValue; + } + + // If valid values are provided, validate against them + if (validValues && !validValues.includes(paramValue as T)) { + return defaultValue; + } + + return paramValue as T; + }; + + const value = ref(getInitialValue()) as Ref; + + // Update URL when value changes + watch(value, (newValue) => { + const currentParam = route.query[paramName]; + + // Only update if the value actually changed + if (currentParam !== newValue) { + const updateMethod = replace ? router.replace : router.push; + updateMethod({ + ...route, + query: { + ...route.query, + [paramName]: newValue, + }, + }); + } + }); + + // Update value when URL query param changes (e.g., browser back/forward) + watch( + () => route.query[paramName], + (newParam) => { + if (!newParam || typeof newParam !== "string") { + // If param is removed, set to default + if (value.value !== defaultValue) { + value.value = defaultValue; + } + return; + } + + // If valid values are provided, validate + if (validValues && !validValues.includes(newParam as T)) { + if (value.value !== defaultValue) { + value.value = defaultValue; + } + return; + } + + // Update value if it changed + if (value.value !== newParam) { + value.value = newParam as T; + } + }, + ); + + // Set initial URL param if not present + if (setInitialParam && !route.query[paramName]) { + const updateMethod = replace ? router.replace : router.push; + updateMethod({ + ...route, + query: { + ...route.query, + [paramName]: value.value, + }, + }); + } + + return value; +} From ae24b972d64a5a9dec8b2451f0e19ecf979cdf1a Mon Sep 17 00:00:00 2001 From: Harsh Tandiya Date: Sun, 28 Dec 2025 21:35:58 +0530 Subject: [PATCH 11/21] chore: upgrade frappe-ui to `v0.1.244` --- frontend/package.json | 2 +- frontend/yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index 39abbff..5b473e4 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -15,7 +15,7 @@ "@vueuse/core": "^13.9.0", "dayjs": "^1.11.19", "feather-icons": "^4.29.2", - "frappe-ui": "^0.1.214", + "frappe-ui": "^0.1.244", "lucide-vue-next": "^0.543.0", "pinia": "^3.0.3", "socket.io-client": "^4.7.2", diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 17bbd24..7ca0050 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -1794,10 +1794,10 @@ fraction.js@^4.3.7: resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.3.7.tgz#06ca0085157e42fda7f9e726e79fefc4068840f7" integrity sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew== -frappe-ui@^0.1.214: - version "0.1.214" - resolved "https://registry.yarnpkg.com/frappe-ui/-/frappe-ui-0.1.214.tgz#844207c000b695cf6c38d0be5058c03a13e42fca" - integrity sha512-1siy9m992OiPqxxjeZ+SapMD4GciD3n45Yuuuua2b+2QgfSD4R9qNTYAU/j4Jj5M/AfMWlx/shuJ6Wq/f6Q8aA== +frappe-ui@^0.1.244: + version "0.1.244" + resolved "https://registry.yarnpkg.com/frappe-ui/-/frappe-ui-0.1.244.tgz#9fdb37a7db08e1e5d8b7033ea77b0977f185b8e9" + integrity sha512-ZO3BDmA5zp655D11Nhw5ZD1f0vppSPAgXW3VxsxnfF0iFymkn2sAZadD+oObJzN2iIpRJ4SHro5UEG40koesOg== dependencies: "@floating-ui/vue" "^1.1.6" "@headlessui/vue" "^1.7.14" From eb8eee6bfa9db3ecf6b97bc7c625fcdc682d8b60 Mon Sep 17 00:00:00 2001 From: Harsh Tandiya Date: Sun, 28 Dec 2025 21:39:20 +0530 Subject: [PATCH 12/21] fix: frappe ui css --- frontend/src/index.css | 57 ++++++++++++++++++++++-------------------- 1 file changed, 30 insertions(+), 27 deletions(-) diff --git a/frontend/src/index.css b/frontend/src/index.css index 61a017b..3e868ef 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -1,35 +1,38 @@ @import "./assets/Inter/inter.css"; -@import "frappe-ui/src/style.css"; -@import url('https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&display=swap'); +@import "frappe-ui/style.css"; +@import url("https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&display=swap"); @font-face { - font-family: "Instrument Serif"; - font-style: normal; - font-weight: 400; - font-display: swap; - src: url("./assets/Instrument_Serif/InstrumentSerif-Regular.woff2") format("woff2"), - url("./assets/Instrument_Serif/InstrumentSerif-Regular.woff") format("woff"); + font-family: "Instrument Serif"; + font-style: normal; + font-weight: 400; + font-display: swap; + src: + url("./assets/Instrument_Serif/InstrumentSerif-Regular.woff2") format("woff2"), + url("./assets/Instrument_Serif/InstrumentSerif-Regular.woff") format("woff"); } .form-description { + @apply w-full p-3; - @apply w-full p-3; + h1, + h2, + h3 { + @apply text-xl font-bold; + } + h4, + h5 { + @apply text-lg font-bold; + } + h6 { + @apply text-base font-bold; + } + p, + li { + @apply text-base font-normal; + } - h1 , h2, h3 { - @apply text-xl font-bold; - } - h4, - h5 { - @apply text-lg font-bold; - } - h6 { - @apply text-base font-bold; - } - p, li { - @apply text-base font-normal; - } - - .is-empty { - @apply text-base font-normal; - } -} \ No newline at end of file + .is-empty { + @apply text-base font-normal; + } +} From 20613e5b53d6e7a53596b33db43debf65ddf5075 Mon Sep 17 00:00:00 2001 From: Harsh Tandiya Date: Tue, 30 Dec 2025 19:42:13 +0530 Subject: [PATCH 13/21] chore: add pydantic[email] dependencies in pyproject --- pyproject.toml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 5338a96..21fb33a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "forms_pro" authors = [ - { name = "Harsh Tandon", email = "harsh@buildwithhussain.com" }, + { name = "Harsh Tandiya", email = "harsh@buildwithhussain.com" }, { name = "BWH Studios", email = "developers@buildwithhussain.com" }, ] description = "Web Forms on steroids!" @@ -11,6 +11,8 @@ dynamic = ["version"] dependencies = [ # "frappe~=15.0.0" # Installed and managed by bench. "Faker~=38.2.0", + "dnspython==2.8.0", + "email-validator==2.3.0", ] [project.urls] From d5b632a7dfbce3b8daaba45c18ab38b16b64f31a Mon Sep 17 00:00:00 2001 From: Harsh Tandiya Date: Wed, 31 Dec 2025 00:12:31 +0530 Subject: [PATCH 14/21] feat: add new global components to Vue declaration - Added AccessSection, Avatar, DescriptionSection, and ShareAccessModal to the global components interface in Vue. - Enhances component accessibility throughout the application. --- frontend/components.d.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/frontend/components.d.ts b/frontend/components.d.ts index e46e47a..d75c5dd 100644 --- a/frontend/components.d.ts +++ b/frontend/components.d.ts @@ -8,7 +8,10 @@ export {} /* prettier-ignore */ declare module 'vue' { export interface GlobalComponents { + AccessSection: typeof import('./src/components/form/manage/AccessSection.vue')['default'] AddFieldsSection: typeof import('./src/components/builder/sidebar/AddFieldsSection.vue')['default'] + Avatar: typeof import('./src/components/ui/Avatar.vue')['default'] + DescriptionSection: typeof import('./src/components/form/manage/DescriptionSection.vue')['default'] DoctypeFieldsSection: typeof import('./src/components/builder/sidebar/DoctypeFieldsSection.vue')['default'] FieldEditorSidebar: typeof import('./src/components/FieldEditorSidebar.vue')['default'] FieldPropertiesForm: typeof import('./src/components/builder/FieldPropertiesForm.vue')['default'] @@ -24,6 +27,7 @@ declare module 'vue' { RouterView: typeof import('vue-router')['RouterView'] Section: typeof import('./src/components/fields/Section.vue')['default'] SettingsSection: typeof import('./src/components/builder/sidebar/SettingsSection.vue')['default'] + ShareAccessModal: typeof import('./src/components/form/manage/ShareAccessModal.vue')['default'] SuccessSection: typeof import('./src/components/submission/SuccessSection.vue')['default'] } } From b3989ea278465589bc7b09aa0868f01bf84574fd Mon Sep 17 00:00:00 2001 From: Harsh Tandiya Date: Wed, 31 Dec 2025 00:20:45 +0530 Subject: [PATCH 15/21] refactor: replace tailwind.config.js with tailwind.config.ts - Converted tailwind configuration from JavaScript to TypeScript for improved type safety and maintainability. - Updated import path for the frappe UI preset. --- frontend/tailwind.config.js | 17 ----------------- frontend/tailwind.config.ts | 17 +++++++++++++++++ 2 files changed, 17 insertions(+), 17 deletions(-) delete mode 100644 frontend/tailwind.config.js create mode 100644 frontend/tailwind.config.ts diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js deleted file mode 100644 index 9a2cd71..0000000 --- a/frontend/tailwind.config.js +++ /dev/null @@ -1,17 +0,0 @@ -import frappeUIPreset from "frappe-ui/src/tailwind/preset"; - -export default { - presets: [frappeUIPreset], - content: [ - "./index.html", - "./src/**/*.{vue,js,ts,jsx,tsx}", - "./node_modules/frappe-ui/src/components/**/*.{vue,js,ts,jsx,tsx}", - ], - theme: { - fontFamily: { - instrument: ["Instrument Serif", "serif"], - }, - extend: {}, - }, - plugins: [], -}; diff --git a/frontend/tailwind.config.ts b/frontend/tailwind.config.ts new file mode 100644 index 0000000..5afef64 --- /dev/null +++ b/frontend/tailwind.config.ts @@ -0,0 +1,17 @@ +import frappeUIPreset from "frappe-ui/tailwind"; + +export default { + presets: [frappeUIPreset], + content: [ + "./index.html", + "./src/**/*.{vue,js,ts,jsx,tsx}", + "./node_modules/frappe-ui/src/components/**/*.{vue,js,ts,jsx,tsx}", + ], + theme: { + fontFamily: { + instrument: ["Instrument Serif", "serif"], + }, + extend: {}, + }, + plugins: [], +}; From 8f601f3732dbb3bc1ff87e35e512bf8f8be8ece8 Mon Sep 17 00:00:00 2001 From: Harsh Tandiya Date: Wed, 31 Dec 2025 01:02:09 +0530 Subject: [PATCH 16/21] refactor: main.js to main.ts --- frontend/index.html | 2 +- frontend/src/{main.js => main.ts} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename frontend/src/{main.js => main.ts} (100%) diff --git a/frontend/index.html b/frontend/index.html index 08c6a25..9f5734d 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -10,7 +10,7 @@
- + \ No newline at end of file diff --git a/frontend/src/main.js b/frontend/src/main.ts similarity index 100% rename from frontend/src/main.js rename to frontend/src/main.ts From 374fed72e7662af97811a624beb859803ffbbad6 Mon Sep 17 00:00:00 2001 From: Harsh Tandiya Date: Thu, 1 Jan 2026 19:59:17 +0530 Subject: [PATCH 17/21] feat: add Avatar component with user information display - Introduced a new Avatar component that fetches and displays user information. - Utilizes TypeScript for improved type safety and integrates with the frappe-ui library. - Enhances user interface by providing a tooltip with the user's full name and an avatar image. --- .pre-commit-config.yaml | 4 +-- frontend/src/components/ui/Avatar.vue | 40 +++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 2 deletions(-) create mode 100644 frontend/src/components/ui/Avatar.vue diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index bb8c19e..277880a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -30,7 +30,7 @@ repos: rev: v2.7.1 hooks: - id: prettier - types_or: [javascript, vue, scss] + types_or: [javascript, vue, scss, ts] # Ignore any files that might contain jinja / bundles exclude: | (?x)^( @@ -45,7 +45,7 @@ repos: rev: v8.44.0 hooks: - id: eslint - types_or: [javascript] + types_or: [javascript, ts] args: ["--quiet"] # Ignore any files that might contain jinja / bundles exclude: | diff --git a/frontend/src/components/ui/Avatar.vue b/frontend/src/components/ui/Avatar.vue new file mode 100644 index 0000000..4160d04 --- /dev/null +++ b/frontend/src/components/ui/Avatar.vue @@ -0,0 +1,40 @@ + + + From c87dc090257a4a189c05d23545b2647916ca552c Mon Sep 17 00:00:00 2001 From: Harsh Tandiya Date: Thu, 1 Jan 2026 20:02:41 +0530 Subject: [PATCH 18/21] refactor: update BaseLayout.vue to use props for sidebar configuration --- frontend/src/layouts/BaseLayout.vue | 89 +++++++++++++++++------------ 1 file changed, 51 insertions(+), 38 deletions(-) diff --git a/frontend/src/layouts/BaseLayout.vue b/frontend/src/layouts/BaseLayout.vue index 5030c3c..04fad08 100644 --- a/frontend/src/layouts/BaseLayout.vue +++ b/frontend/src/layouts/BaseLayout.vue @@ -2,9 +2,9 @@
@@ -24,47 +24,60 @@ import { session } from "@/data/session"; import { ref, computed } from "vue"; import { Sidebar } from "frappe-ui"; import { LayoutDashboard, ArrowLeftRight, LogOut } from "lucide-vue-next"; -import toggleTheme from "@/utils/theme"; import { useUser } from "@/stores/user"; + const userStore = useUser(); -const menuItems = computed(() => { - return [ - // { - // label: "Switch Teams", - // icon: ArrowLeftRight, - // onClick: () => { - // console.log("Switch Teams"); - // }, - // }, - { - label: "Log out", - icon: LogOut, - onClick: session.logout.submit, - }, - ]; +const props = defineProps({ + sidebarHeader: { + type: Object, + default: null, + }, + sidebarSections: { + type: Array, + default: [], + }, }); -const sidebarSections = ref([ +const defaultSidebarMenuItems = computed(() => [ { - label: "", - items: [ - { - label: "Dashboard", - to: "/", - isActive: true, - icon: LayoutDashboard, - }, - ], + label: "Log out", + icon: LogOut, + onClick: session.logout.submit, }, - // { - // label: "", - // items: [ - // { - // label: "Submissions", - // to: "/submissions", - // }, - // ], - // }, ]); + +const sidebarHeader = computed(() => { + if (props.sidebarHeader) { + return props.sidebarHeader; + } + + return { + title: "Forms Pro", + subtitle: session.full_name, + menuItems: defaultSidebarMenuItems.value, + }; +}); + +const sidebarSections = computed(() => { + if (props.sidebarSections && props.sidebarSections.length > 0) { + return props.sidebarSections; + } + + const _sections = [ + { + label: "", + items: [ + { + label: "Dashboard", + to: "/", + isActive: true, + icon: LayoutDashboard, + }, + ], + }, + ]; + + return _sections; +}); From d57226f102504484e86941dee6b5442c36fe609d Mon Sep 17 00:00:00 2001 From: Harsh Tandiya Date: Thu, 1 Jan 2026 20:03:00 +0530 Subject: [PATCH 19/21] refactor: clean up date utility functions by removing redundant type annotations --- frontend/src/utils/date.ts | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/frontend/src/utils/date.ts b/frontend/src/utils/date.ts index 85c5677..64ae3f0 100644 --- a/frontend/src/utils/date.ts +++ b/frontend/src/utils/date.ts @@ -18,7 +18,7 @@ dayjs.extend(customParseFormat); */ export function formatDate( date: string | Date | dayjs.Dayjs | null | undefined, - format: string = "MMM D, YYYY", + format: string = "MMM D, YYYY" ): string { if (!date) return ""; return dayjs(date).format(format); @@ -32,7 +32,7 @@ export function formatDate( */ export function formatDateTime( date: string | Date | dayjs.Dayjs | null | undefined, - format: string = "MMM D, YYYY h:mm A", + format: string = "MMM D, YYYY h:mm A" ): string { if (!date) return ""; return dayjs(date).format(format); @@ -45,7 +45,7 @@ export function formatDateTime( * @returns Pretty formatted date string */ export function formatPrettyDate( - date: string | Date | dayjs.Dayjs | null | undefined, + date: string | Date | dayjs.Dayjs | null | undefined ): string { if (!date) return ""; @@ -80,7 +80,7 @@ export function formatPrettyDate( */ export function formatTime( date: string | Date | dayjs.Dayjs | null | undefined, - format: string = "h:mm A", + format: string = "h:mm A" ): string { if (!date) return ""; return dayjs(date).format(format); @@ -92,7 +92,7 @@ export function formatTime( * @returns Relative time string */ export function getRelativeTime( - date: string | Date | dayjs.Dayjs | null | undefined, + date: string | Date | dayjs.Dayjs | null | undefined ): string { if (!date) return ""; return dayjs(date).fromNow(); @@ -104,7 +104,7 @@ export function getRelativeTime( * @returns True if the date is today */ export function isDateToday( - date: string | Date | dayjs.Dayjs | null | undefined, + date: string | Date | dayjs.Dayjs | null | undefined ): boolean { if (!date) return false; return dayjs(date).isToday(); @@ -116,7 +116,7 @@ export function isDateToday( * @returns True if the date is in the past */ export function isDatePast( - date: string | Date | dayjs.Dayjs | null | undefined, + date: string | Date | dayjs.Dayjs | null | undefined ): boolean { if (!date) return false; return dayjs(date).isBefore(dayjs()); @@ -128,7 +128,7 @@ export function isDatePast( * @returns True if the date is in the future */ export function isDateFuture( - date: string | Date | dayjs.Dayjs | null | undefined, + date: string | Date | dayjs.Dayjs | null | undefined ): boolean { if (!date) return false; return dayjs(date).isAfter(dayjs()); @@ -144,7 +144,7 @@ export function isDateFuture( export function getDateDiff( date1: string | Date | dayjs.Dayjs | null | undefined, date2: string | Date | dayjs.Dayjs | null | undefined = dayjs(), - unit: dayjs.ManipulateType = "day", + unit: dayjs.ManipulateType = "day" ): number { if (!date1) return 0; return dayjs(date1).diff(dayjs(date2), unit); @@ -156,7 +156,7 @@ export function getDateDiff( * @returns Short formatted date string */ export function formatShortDate( - date: string | Date | dayjs.Dayjs | null | undefined, + date: string | Date | dayjs.Dayjs | null | undefined ): string { if (!date) return ""; return dayjs(date).format("M/D/YYYY"); @@ -168,7 +168,7 @@ export function formatShortDate( * @returns ISO formatted date string */ export function formatISODate( - date: string | Date | dayjs.Dayjs | null | undefined, + date: string | Date | dayjs.Dayjs | null | undefined ): string { if (!date) return ""; return dayjs(date).format("YYYY-MM-DD"); From 303e6fb0b3ca1384ef30652ec027325f45cfa816 Mon Sep 17 00:00:00 2001 From: Harsh Tandiya Date: Thu, 1 Jan 2026 20:03:56 +0530 Subject: [PATCH 20/21] feat: manage form v1 --- forms_pro/api/form.py | 88 +++++++-- forms_pro/api/team.py | 24 +++ forms_pro/api/user.py | 16 +- forms_pro/forms_pro/doctype/form/form.py | 11 ++ .../forms_pro/doctype/fp_team/fp_team.py | 26 ++- frontend/.gitignore | 3 +- frontend/auto-imports.d.ts | 4 +- frontend/components.d.ts | 3 +- .../src/components/FormBuilderContent.vue | 3 +- .../components/dashboard/FormPreviewCard.vue | 2 +- .../components/form/manage/AccessSection.vue | 123 +++++++++++++ .../form/manage/DescriptionSection.vue | 11 ++ .../form/manage/RemoveAccessModal.vue | 45 +++++ .../form/manage/ShareAccessModal.vue | 108 +++++++++++ frontend/src/composables/useQueryParam.ts | 4 +- frontend/src/main.ts | 42 ++--- frontend/src/pages/manage/ManageForm.vue | 79 ++++++++ .../src/pages/manage/overview/Overview.vue | 71 ++++++++ frontend/src/router.js | 13 ++ frontend/src/stores/editForm.ts | 13 +- frontend/src/stores/form/manageForm.ts | 170 ++++++++++++++++++ frontend/src/stores/submissionForm.ts | 2 +- frontend/src/stores/team.ts | 41 +++++ frontend/src/stores/user.ts | 4 +- frontend/src/utils/form_fields.ts | 3 +- frontend/src/utils/form_generator.ts | 4 +- frontend/src/utils/user.ts | 18 ++ frontend/tsconfig.json | 2 +- 28 files changed, 873 insertions(+), 60 deletions(-) create mode 100644 frontend/src/components/form/manage/AccessSection.vue create mode 100644 frontend/src/components/form/manage/DescriptionSection.vue create mode 100644 frontend/src/components/form/manage/RemoveAccessModal.vue create mode 100644 frontend/src/components/form/manage/ShareAccessModal.vue create mode 100644 frontend/src/pages/manage/ManageForm.vue create mode 100644 frontend/src/pages/manage/overview/Overview.vue create mode 100644 frontend/src/stores/form/manageForm.ts create mode 100644 frontend/src/stores/team.ts create mode 100644 frontend/src/utils/user.ts diff --git a/forms_pro/api/form.py b/forms_pro/api/form.py index 6569502..f477d0b 100644 --- a/forms_pro/api/form.py +++ b/forms_pro/api/form.py @@ -1,4 +1,19 @@ import frappe +from frappe.share import remove +from pydantic import BaseModel, Field + +from forms_pro.api.user import get_user +from forms_pro.forms_pro.doctype.form.form import Form + + +class FormSharedWithResponse(BaseModel): + full_name: str + user_image: str | None + email: str = Field(alias="user") + read: bool + write: bool + share: bool + submit: bool @frappe.whitelist(allow_guest=True) @@ -9,7 +24,7 @@ def get_form_by_route(route: str) -> dict: @frappe.whitelist(allow_guest=True) def get_form(form_id: str) -> dict: - form = frappe.get_doc( + form: Form = frappe.get_doc( "Form", form_id, ) @@ -24,6 +39,66 @@ def get_form(form_id: str) -> dict: } +@frappe.whitelist() +def get_form_shared_with(form_id: str) -> list[frappe.Any]: + """ + Get list of users with which a form is shared. + + We validate the current user has read access to the form. + """ + if not frappe.has_permission( + "Form", + "read", + form_id, + ): + frappe.throw("You do not have read access to this form") + + form: Form = frappe.get_doc("Form", form_id) + shared_with = form.shared_with() + + shared_with_responses = [] + + for user in shared_with: + _user = get_user(user["user"]) + user.update(_user) + shared_with_responses.append(FormSharedWithResponse.model_validate(user).model_dump()) + + return shared_with_responses + + +@frappe.whitelist() +def remove_form_access(form_id: str, user_email: str) -> None: + """ + Remove access to a form for a user. + + We validate the current user has write access to the form. + + args: + form_id: str - The ID of the form to remove access to. + user_email: str - The email of the user to remove access to. + + """ + + if not frappe.has_permission("Form", "write", form_id): + frappe.throw("You do not have write access to this form") + + return remove(doctype="Form", name=form_id, user=user_email, flags={"ignore_permissions": True}) + + +@frappe.whitelist() +def get_doctype_list() -> list[str]: + if not frappe.has_permission("DocType", "read"): + frappe.throw("You do not have read access to this doctype") + + return frappe.db.get_list( + "DocType", + filters={"istable": 0}, + pluck="name", + order_by="name", + limit_page_length=99999, + ) + + @frappe.whitelist() def get_doctype_fields(doctype: str) -> dict: doctype = frappe.get_doc("DocType", doctype) @@ -43,14 +118,3 @@ def get_doctype_fields(doctype: str) -> dict: fields = [field for field in fields if field.fieldtype not in FIELDTYPES_TO_REMOVE] return fields - - -@frappe.whitelist() -def get_doctype_list() -> list[str]: - return frappe.db.get_list( - "DocType", - filters={"istable": 0}, - pluck="name", - order_by="name", - limit_page_length=99999, - ) diff --git a/forms_pro/api/team.py b/forms_pro/api/team.py index 4358c5a..58bfa09 100644 --- a/forms_pro/api/team.py +++ b/forms_pro/api/team.py @@ -1,5 +1,6 @@ import frappe +from forms_pro.forms_pro.doctype.fp_team.fp_team import FPTeam, GetTeamMembersResponse from forms_pro.utils.teams import GetTeamFormsResponseSchema from forms_pro.utils.teams import get_team_forms as get_team_forms_utils @@ -17,3 +18,26 @@ def get_team_forms(team_id: str) -> list[GetTeamFormsResponseSchema]: """ forms = get_team_forms_utils(team_id=team_id) return forms + + +@frappe.whitelist() +def get_team_members(team_id: str) -> list[GetTeamMembersResponse]: + """ + + Get the list of team members in a FP Team. + This endpoint checks if the session user has the permission to read this FP Team DocType + + """ + frappe.has_permission( + doctype="FP Team", + ptype="read", + doc=team_id, + user=frappe.session.user, + throw=True, + ) + print("team_id", team_id) + + team: FPTeam = frappe.get_doc("FP Team", team_id) + members = team.team_members + + return members diff --git a/forms_pro/api/user.py b/forms_pro/api/user.py index 3ffc590..11c49f7 100644 --- a/forms_pro/api/user.py +++ b/forms_pro/api/user.py @@ -30,8 +30,22 @@ def extract_roles(cls, v: list[HasRole]) -> list[str]: return [role.role for role in v] +class GetUserBasicResponse(BaseModel): + full_name: str + user_image: str | None = None + + +@frappe.whitelist() +def get_user(user: str) -> GetUserBasicResponse | None: + """Get basic user data for a given user""" + data = frappe.db.get_value("User", user, ["full_name", "user_image"], as_dict=True) + if not data: + return None + return GetUserBasicResponse.model_validate(data).model_dump() + + @frappe.whitelist() -def get_user() -> GetUserResponseSchema: +def get_current_user() -> GetUserResponseSchema: """ Get Current User Data """ diff --git a/forms_pro/forms_pro/doctype/form/form.py b/forms_pro/forms_pro/doctype/form/form.py index 9378aba..a85deaa 100644 --- a/forms_pro/forms_pro/doctype/form/form.py +++ b/forms_pro/forms_pro/doctype/form/form.py @@ -1,8 +1,11 @@ # Copyright (c) 2025, harsh@buildwithhussain.com and contributors # For license information, please see license.txt +from typing import Any + import frappe from frappe.model.document import Document +from frappe.share import get_users class Form(Document): @@ -34,6 +37,14 @@ class Form(Document): def linked_doctype_doc(self) -> Document: return frappe.get_doc("DocType", self.linked_doctype) + @frappe.whitelist() + def shared_with(self) -> list[dict[str, Any]]: + """ + Get list of users with which this form is shared + """ + users_shared_with = get_users(self.doctype, self.name) + return users_shared_with + def generate_initial_route(self) -> str: return "s/forms_pro_" + frappe.utils.random_string(8) 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 f31c501..e717cb8 100644 --- a/forms_pro/forms_pro/doctype/fp_team/fp_team.py +++ b/forms_pro/forms_pro/doctype/fp_team/fp_team.py @@ -3,6 +3,16 @@ # import frappe from frappe.model.document import Document +from frappe.utils import cached_property +from pydantic import BaseModel, EmailStr + +from forms_pro.api.user import get_user + + +class GetTeamMembersResponse(BaseModel): + full_name: str + user_image: str | None + email: EmailStr class FPTeam(Document): @@ -20,15 +30,25 @@ class FPTeam(Document): users: DF.TableMultiSelect[FPTeamMember] # end: auto-generated types - @property - def team_members(self) -> list[str]: + @cached_property + def team_members(self) -> list[GetTeamMembersResponse]: """ Get the list of team members Returns: list[str] - List of team member email addresses """ - return [member.user for member in self.users] if self.users else [] + if not len(self.users): + return [] + + members = [] + + for member in self.users: + _user = get_user(member.user) + _user["email"] = member.user + members.append(GetTeamMembersResponse.model_validate(_user).model_dump()) + + return members def is_team_member(self, user: str) -> bool: """ diff --git a/frontend/.gitignore b/frontend/.gitignore index cb723df..bfaab69 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -5,4 +5,5 @@ dist-ssr *.local coverage ../ -sites \ No newline at end of file +sites +src/types/frappe-ui.d.ts \ No newline at end of file diff --git a/frontend/auto-imports.d.ts b/frontend/auto-imports.d.ts index 9d24007..b0b0357 100644 --- a/frontend/auto-imports.d.ts +++ b/frontend/auto-imports.d.ts @@ -5,6 +5,4 @@ // Generated by unplugin-auto-import // biome-ignore lint: disable export {} -declare global { - -} +declare global {} diff --git a/frontend/components.d.ts b/frontend/components.d.ts index d75c5dd..9ec01ed 100644 --- a/frontend/components.d.ts +++ b/frontend/components.d.ts @@ -3,7 +3,7 @@ // Generated by unplugin-vue-components // Read more: https://github.com/vuejs/core/pull/3399 // biome-ignore lint: disable -export {} +export {}; /* prettier-ignore */ declare module 'vue' { @@ -22,6 +22,7 @@ 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'] + RemoveAccessModal: typeof import('./src/components/form/manage/RemoveAccessModal.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/src/components/FormBuilderContent.vue b/frontend/src/components/FormBuilderContent.vue index 6e6559a..d67a833 100644 --- a/frontend/src/components/FormBuilderContent.vue +++ b/frontend/src/components/FormBuilderContent.vue @@ -1,6 +1,6 @@