From 3263b5414116dbfb81567ecd138a2852658ceee7 Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Tue, 10 Dec 2024 01:10:14 +0530 Subject: [PATCH 01/86] chore: update lucide --- frontend/lucideIcons.js | 8 +++++++- frontend/package.json | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/frontend/lucideIcons.js b/frontend/lucideIcons.js index a328a99c..e666a73a 100644 --- a/frontend/lucideIcons.js +++ b/frontend/lucideIcons.js @@ -2,10 +2,10 @@ import * as LucideIcons from 'lucide-static' let icons = {} for (const icon in LucideIcons) { - let iconSvg = LucideIcons[icon] if (icon == 'default') { continue } + let iconSvg = LucideIcons[icon] // set stroke-width to 1.5 if (iconSvg && iconSvg.includes('stroke-width')) { @@ -26,8 +26,14 @@ export default icons function camelToDash(key) { // barChart2 -> bar-chart-2 let withNumber = key.replace(/[A-Z0-9]/g, (m) => '-' + m.toLowerCase()) + if (withNumber.startsWith('-')) { + withNumber = withNumber.substring(1) + } // barChart2 -> bar-chart2 let withoutNumber = key.replace(/[A-Z]/g, (m) => '-' + m.toLowerCase()) + if (withoutNumber.startsWith('-')) { + withoutNumber = withoutNumber.substring(1) + } if (withNumber !== withoutNumber) { // both are required because unplugin icon resolver doesn't put a dash before numbers diff --git a/frontend/package.json b/frontend/package.json index 5187bb3d..1ac42c9f 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -29,7 +29,7 @@ "@vitejs/plugin-vue-jsx": "^3.0.1", "autoprefixer": "^10.4.2", "cypress": "10.11.0", - "lucide-static": "^0.257.0", + "lucide-static": "^0.468.0", "postcss": "^8.4.5", "prettier": "^3.3.3", "prettier-plugin-tailwindcss": "^0.6.8", From 24b6314edfc49462c5d73f05de83391a8b573c21 Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Tue, 10 Dec 2024 01:12:23 +0530 Subject: [PATCH 02/86] chore: add tsconfig.json --- frontend/tsconfig.json | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 frontend/tsconfig.json diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 00000000..a63ff5df --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ESNext", + "useDefineForClassFields": true, + "module": "ESNext", + "moduleResolution": "Node", + "strict": true, + "jsx": "preserve", + "resolveJsonModule": true, + "isolatedModules": true, + "esModuleInterop": true, + "lib": ["ESNext", "DOM"], + "skipLibCheck": true, + "noEmit": true, + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "**/*.spec.ts"] +} From 334677a88a6db92c4f3b99dfd9ce713536063228 Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Tue, 10 Dec 2024 01:16:33 +0530 Subject: [PATCH 03/86] fix: space list view - join/leave actions - create new space --- frontend/components.d.ts | 1 + frontend/src/components/NewSpaceDialog.vue | 66 ++++++++ frontend/src/data/newDoc.js | 21 +++ frontend/src/data/projects.js | 1 + frontend/src/data/{users.js => users.ts} | 4 + frontend/src/pages/SpaceList.vue | 152 ++++++++++++++++++ frontend/src/router.js | 5 + .../gameplan/doctype/gp_project/gp_project.py | 17 ++ 8 files changed, 267 insertions(+) create mode 100644 frontend/src/components/NewSpaceDialog.vue create mode 100644 frontend/src/data/newDoc.js rename frontend/src/data/{users.js => users.ts} (93%) create mode 100644 frontend/src/pages/SpaceList.vue diff --git a/frontend/components.d.ts b/frontend/components.d.ts index 237a4b84..a50e8a49 100644 --- a/frontend/components.d.ts +++ b/frontend/components.d.ts @@ -74,6 +74,7 @@ declare module 'vue' { LucideX: (typeof import('~icons/lucide/x'))['default'] Members: (typeof import('./src/components/Settings/Members.vue'))['default'] MobileLayout: (typeof import('./src/components/MobileLayout.vue'))['default'] +NewSpaceDialog: (typeof import('./src/components/NewSpaceDialog.vue'))['default'] NewTaskDialog: (typeof import('./src/components/NewTaskDialog.vue'))['default'] PageList: (typeof import('./src/components/PageList.vue'))['default'] Pie: (typeof import('./src/components/Pie.vue'))['default'] diff --git a/frontend/src/components/NewSpaceDialog.vue b/frontend/src/components/NewSpaceDialog.vue new file mode 100644 index 00000000..4595096d --- /dev/null +++ b/frontend/src/components/NewSpaceDialog.vue @@ -0,0 +1,66 @@ + + diff --git a/frontend/src/data/newDoc.js b/frontend/src/data/newDoc.js new file mode 100644 index 00000000..4848af5f --- /dev/null +++ b/frontend/src/data/newDoc.js @@ -0,0 +1,21 @@ +import { createResource } from 'frappe-ui' +import { unref, reactive } from 'vue' + +export function useNewDoc(doctype, doc = {}) { + doc = reactive(doc) + const resource = createResource({ + url: 'frappe.client.insert', + makeParams() { + let values = unref(doc) + return { + doc: { + doctype, + ...values, + }, + } + }, + }) + + resource.doc = doc + return resource +} diff --git a/frontend/src/data/projects.js b/frontend/src/data/projects.js index 2589460c..b7d76cec 100644 --- a/frontend/src/data/projects.js +++ b/frontend/src/data/projects.js @@ -13,6 +13,7 @@ export let projects = createListResource({ 'modified', 'tasks_count', 'discussions_count', + { members: ['user'] }, ], orderBy: 'title asc', pageLength: 999, diff --git a/frontend/src/data/users.js b/frontend/src/data/users.ts similarity index 93% rename from frontend/src/data/users.js rename to frontend/src/data/users.ts index 64e65d0b..d1086206 100644 --- a/frontend/src/data/users.js +++ b/frontend/src/data/users.ts @@ -43,3 +43,7 @@ export function getUser(email) { export let activeUsers = computed(() => { return users.data.filter((user) => user.enabled) }) + +export function useSessionUser() { + return getUser('sessionUser') +} diff --git a/frontend/src/pages/SpaceList.vue b/frontend/src/pages/SpaceList.vue new file mode 100644 index 00000000..dc39bdaa --- /dev/null +++ b/frontend/src/pages/SpaceList.vue @@ -0,0 +1,152 @@ + + diff --git a/frontend/src/router.js b/frontend/src/router.js index c6f0f7a0..246daea4 100644 --- a/frontend/src/router.js +++ b/frontend/src/router.js @@ -71,6 +71,11 @@ const routes = [ name: 'Teams', component: () => import('@/pages/Teams.vue'), }, + { + path: '/spaces', + name: 'Spaces', + component: () => import('@/pages/SpaceList.vue'), + }, { path: '/people/:personId', name: 'PersonProfile', diff --git a/gameplan/gameplan/doctype/gp_project/gp_project.py b/gameplan/gameplan/doctype/gp_project/gp_project.py index 1bfd8ca5..acadea51 100644 --- a/gameplan/gameplan/doctype/gp_project/gp_project.py +++ b/gameplan/gameplan/doctype/gp_project/gp_project.py @@ -213,6 +213,23 @@ def unfollow(self): ) frappe.delete_doc("GP Followed Project", follow_id) + @frappe.whitelist() + def join(self): + user = frappe.session.user + users = [d.user for d in self.members] + if user not in users: + self.append("members", {"user": user}) + self.save() + + @frappe.whitelist() + def leave(self): + user = frappe.session.user + for member in self.members: + if member.user == user: + self.remove(member) + self.save() + break + def get_meta_tags(url): response = requests.get(url, timeout=2, allow_redirects=True) From 159cbe44a80f66f7ad8e67b8aac76812786aceed Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Tue, 10 Dec 2024 02:38:36 +0530 Subject: [PATCH 04/86] fix: show only joined spaces in sidebar - refactor sidebar code into composition api --- frontend/src/components/AppSidebar.vue | 305 ++++++------------ .../src/components/DropdownMoreOptions.vue | 15 + frontend/src/components/IconPicker.vue | 4 +- frontend/src/components/NewSpaceDialog.vue | 4 +- frontend/src/data/teams.js | 2 +- frontend/src/utils/sidebarResize.ts | 45 +++ 6 files changed, 172 insertions(+), 203 deletions(-) create mode 100644 frontend/src/components/DropdownMoreOptions.vue create mode 100644 frontend/src/utils/sidebarResize.ts diff --git a/frontend/src/components/AppSidebar.vue b/frontend/src/components/AppSidebar.vue index 9b2f6abd..d0fa37bb 100644 --- a/frontend/src/components/AppSidebar.vue +++ b/frontend/src/components/AppSidebar.vue @@ -62,62 +62,57 @@
-

Teams

- +

Spaces

+
Date: Wed, 8 Jan 2025 12:30:04 +0530 Subject: [PATCH 43/86] fix: update joined spaces on join/leave --- frontend/src/data/spaces.ts | 4 ++++ frontend/src/pages/SpaceList.vue | 27 ++++++++++++++------------- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/frontend/src/data/spaces.ts b/frontend/src/data/spaces.ts index 6adb3043..5c679fd4 100644 --- a/frontend/src/data/spaces.ts +++ b/frontend/src/data/spaces.ts @@ -60,3 +60,7 @@ export const joinedSpaces = useCall({ cacheKey: 'joinedSpaces', initialData: [], }) + +export function hasJoined(spaceId: MaybeRefOrGetter) { + return joinedSpaces.data?.includes(toValue(spaceId)) +} diff --git a/frontend/src/pages/SpaceList.vue b/frontend/src/pages/SpaceList.vue index d767354d..77f40ce2 100644 --- a/frontend/src/pages/SpaceList.vue +++ b/frontend/src/pages/SpaceList.vue @@ -55,7 +55,7 @@
+
+ +
@@ -36,6 +39,7 @@ import { computed, ref } from 'vue' import { Breadcrumbs, Select, TabButtons, usePageMeta } from 'frappe-ui' import DiscussionList from '@/components/DiscussionList.vue' +import LastPostReminder from '@/components/LastPostReminder.vue' const feedType = ref('following') const orderBy = ref('last_post_at desc') diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 8229d5f5..e246414d 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -2681,10 +2681,10 @@ lru-cache@^6.0.0: dependencies: yallist "^4.0.0" -lucide-static@^0.468.0: - version "0.468.0" - resolved "https://registry.yarnpkg.com/lucide-static/-/lucide-static-0.468.0.tgz#f4e5525123fff588decd8e1aa10856842b107b78" - integrity sha512-JvpWui2umxRyEVMoETfMzb+qKqibV/sdoqJbKmW1JLdkuhXluJKoO6NqCbvCK/vAbUuH5bTEFD4T6uECsrNcnA== +lucide-static@^0.469.0: + version "0.469.0" + resolved "https://registry.yarnpkg.com/lucide-static/-/lucide-static-0.469.0.tgz#edf4eb55476fa1b2ae1e1c1922152365dec66854" + integrity sha512-ravTgZodIVLO53rJyjIq0iuCRHs+kjd3dAfOcTbA41KCDfdy0Pt6Me8akkhda3jRpbIxjKe1PpYZOg2eQnI/lA== magic-string@^0.30.0: version "0.30.0" diff --git a/gameplan/gameplan/doctype/gp_user_profile/gp_user_profile.py b/gameplan/gameplan/doctype/gp_user_profile/gp_user_profile.py index 40da0454..4a7e672c 100644 --- a/gameplan/gameplan/doctype/gp_user_profile/gp_user_profile.py +++ b/gameplan/gameplan/doctype/gp_user_profile/gp_user_profile.py @@ -158,3 +158,16 @@ def remove_imgbg_in_background(profile_name, default_color=None): profile.image_background_color = default_color profile.save() gameplan.refetch_resource("Users", user=profile.user) + + +@frappe.whitelist() +def get_last_post(): + result = frappe.db.get_list( + "GP Discussion", + filters={"owner": frappe.session.user}, + fields=["creation"], + order_by="creation desc", + limit=1, + pluck="creation", + ) + return result[0] if result else None From 1edf759df5b193ab0ce15225686632d629c2525f Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Wed, 8 Jan 2025 13:46:33 +0530 Subject: [PATCH 45/86] fix: revert list return format --- gameplan/gameplan/doctype/gp_discussion/api.py | 4 +++- gameplan/gameplan/doctype/gp_task/gp_task.py | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/gameplan/gameplan/doctype/gp_discussion/api.py b/gameplan/gameplan/doctype/gp_discussion/api.py index 96c9196a..3baa4d97 100644 --- a/gameplan/gameplan/doctype/gp_discussion/api.py +++ b/gameplan/gameplan/doctype/gp_discussion/api.py @@ -95,7 +95,9 @@ def get_discussions(filters=None, order_by=None, start=None, limit=None): ) for discussion in discussions: discussion["ongoing_polls"] = [p for p in ongoing_polls if str(p.discussion) == str(discussion.name)] - return {"result": discussions, "has_next_page": has_next_page} + + frappe.response["has_next_page"] = has_next_page + return discussions def clause_discussions_commented_by_user(user): diff --git a/gameplan/gameplan/doctype/gp_task/gp_task.py b/gameplan/gameplan/doctype/gp_task/gp_task.py index d3ee66d2..732d9370 100644 --- a/gameplan/gameplan/doctype/gp_task/gp_task.py +++ b/gameplan/gameplan/doctype/gp_task/gp_task.py @@ -102,4 +102,5 @@ def get_list( query = query.where((Task.assigned_to == assigned_or_owner) | (Task.owner == assigned_or_owner)) data = query.run(as_dict=True, debug=debug) - return {"result": data[:limit], "has_next_page": len(data) > limit} + frappe.response["has_next_page"] = len(data) > limit + return data[:limit] From 2a74a2a1bd3a22d0428cbdd76f028a185ffefaeb Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Wed, 8 Jan 2025 14:01:08 +0530 Subject: [PATCH 46/86] feat: clear cache functionality --- frontend/src/components/UserDropdown.vue | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/frontend/src/components/UserDropdown.vue b/frontend/src/components/UserDropdown.vue index 30308547..566eda3d 100644 --- a/frontend/src/components/UserDropdown.vue +++ b/frontend/src/components/UserDropdown.vue @@ -23,9 +23,11 @@ import { Dropdown } from 'frappe-ui' import { showSettingsDialog } from '@/components/Settings/SettingsDialog.vue' import LucideCreditCard from '~icons/lucide/credit-card' import LucideMoon from '~icons/lucide/moon' +import LucideListRestart from '~icons/lucide/list-restart' import GameplanLogo from './GameplanLogo.vue' import { useUser } from '@/data/users' import { session } from '@/data/session' +import { clear as clearIndexDb } from 'idb-keyval' const user = useUser() @@ -49,6 +51,11 @@ const dropdownItems = computed(() => [ label: 'Toggle theme', onClick: toggleTheme, }, + { + icon: LucideListRestart, + label: 'Clear cache', + onClick: clearCache, + }, { icon: () => h(LucideCreditCard), label: 'Subscription', @@ -71,6 +78,15 @@ function toggleTheme() { localStorage.setItem('theme', theme) } +function clearCache() { + localStorage.clear() + sessionStorage.clear() + clearIndexDb().then(() => { + console.log('Cache cleared') + window.location.reload() + }) +} + onMounted(() => { const theme = localStorage.getItem('theme') if (['light', 'dark'].includes(theme)) { From f8ac585cd4e6c39edd328f12c17a83eac0abe48c Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Wed, 8 Jan 2025 14:13:55 +0530 Subject: [PATCH 47/86] fix: remove projects usage --- frontend/src/components/MergeSpaceDialog.vue | 56 +++++++++----------- 1 file changed, 24 insertions(+), 32 deletions(-) diff --git a/frontend/src/components/MergeSpaceDialog.vue b/frontend/src/components/MergeSpaceDialog.vue index 1b88e4ae..206d84d7 100644 --- a/frontend/src/components/MergeSpaceDialog.vue +++ b/frontend/src/components/MergeSpaceDialog.vue @@ -6,7 +6,6 @@ @after-leave=" () => { selectedSpace = null - projects.runDocMethod.reset() } " v-model="show" @@ -26,25 +25,16 @@ {{ option.icon }} - + @@ -54,14 +44,17 @@ import { computed, ref } from 'vue' import { useRouter } from 'vue-router' import { Autocomplete } from 'frappe-ui' import { useGroupedSpaces } from '@/data/groupedSpaces' -import { projects, getProject } from '@/data/projects' +import { useDoctype } from 'frappe-ui/src/data-fetching' +import { GPProject } from '@/types/doctypes' +import { useSpace } from '@/data/spaces' const props = defineProps<{ - spaceId: string | number + spaceId: string }>() const router = useRouter() -const space = computed(() => getProject(props.spaceId)) +const spaces = useDoctype('GP Project') +const space = useSpace(() => props.spaceId) const selectedSpace = ref(null) const show = defineModel() @@ -82,28 +75,27 @@ const groupedSpaceOptions = computed(() => { }) function submit() { - projects.runDocMethod.submit( - { + spaces.runDocMethod + .submit({ method: 'merge_with_project', name: props.spaceId, - project: selectedSpace.value?.value, - }, - { + params: { + project: selectedSpace.value?.value, + }, validate() { if (!selectedSpace.value?.value) { return 'Please select a project to merge' } }, - onSuccess() { - if (selectedSpace.value) { - show.value = false - return router.replace({ - name: 'Space', - params: { spaceId: selectedSpace.value.value }, - }) - } - }, - }, - ) + }) + .then(() => { + if (selectedSpace.value) { + show.value = false + return router.replace({ + name: 'Space', + params: { spaceId: selectedSpace.value.value }, + }) + } + }) } From 25c4c799b3da489428f890c4ec16b82d2b3e28f4 Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Wed, 8 Jan 2025 14:14:11 +0530 Subject: [PATCH 48/86] fix: disable projects fetch --- frontend/src/components/Settings/InvitePeople.vue | 2 +- frontend/src/data/projects.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/Settings/InvitePeople.vue b/frontend/src/components/Settings/InvitePeople.vue index 1d913413..6bfa2e46 100644 --- a/frontend/src/components/Settings/InvitePeople.vue +++ b/frontend/src/components/Settings/InvitePeople.vue @@ -94,7 +94,7 @@ From e48e49b8d35893167dd7d56d2a1f84cdb57554e5 Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Wed, 8 Jan 2025 14:40:35 +0530 Subject: [PATCH 50/86] fix: update frappe-ui --- 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 2645f49b..05102f37 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -15,7 +15,7 @@ "@vueuse/core": "^10.1.2", "dayjs": "^1.10.7", "feather-icons": "^4.28.0", - "frappe-ui": "^0.1.97", + "frappe-ui": "^0.1.101", "fuzzysort": "^2.0.4", "gemoji": "^7.1.0", "htmldiff-js": "^1.0.5", diff --git a/frontend/yarn.lock b/frontend/yarn.lock index e246414d..f54d853c 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -2108,10 +2108,10 @@ fraction.js@^4.2.0: resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.2.0.tgz#448e5109a313a3527f5a3ab2119ec4cf0e0e2950" integrity sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA== -frappe-ui@^0.1.97: - version "0.1.97" - resolved "https://registry.yarnpkg.com/frappe-ui/-/frappe-ui-0.1.97.tgz#8eb9532ae9c916fcd15e8798b8e1376bd4d73a59" - integrity sha512-jYbSm2703nHS+pLOGs2lXNCNLZmhmVaS25fcysax5wzcAXiWT7ZC2iRRvTaHKA9znJxnXXgeaMV37Z+zeWSpdA== +frappe-ui@^0.1.101: + version "0.1.101" + resolved "https://registry.yarnpkg.com/frappe-ui/-/frappe-ui-0.1.101.tgz#12077b4ca18574c11e95dae40df59fdadfada16f" + integrity sha512-IZb0KKQz/fcgnn2Q6MZoLImZECN9/cvHbyD/kpAGQlAHqOTKw6cdZMNq93CwdGZQgKfdH3p/2dqu6G6jP59PtQ== dependencies: "@headlessui/vue" "^1.7.14" "@popperjs/core" "^2.11.2" From 465ad051433e748f9ade84706ff1f58e4d45e851 Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Wed, 8 Jan 2025 15:07:33 +0530 Subject: [PATCH 51/86] fix: spacing --- frontend/src/components/LastPostReminder.vue | 39 ++++++++++---------- frontend/src/pages/Discussions.vue | 5 +-- 2 files changed, 21 insertions(+), 23 deletions(-) diff --git a/frontend/src/components/LastPostReminder.vue b/frontend/src/components/LastPostReminder.vue index 8d6f43d7..5c1f2127 100644 --- a/frontend/src/components/LastPostReminder.vue +++ b/frontend/src/components/LastPostReminder.vue @@ -1,27 +1,26 @@ diff --git a/frontend/src/data/teams.ts b/frontend/src/data/teams.ts index 9cb07d54..89698372 100644 --- a/frontend/src/data/teams.ts +++ b/frontend/src/data/teams.ts @@ -16,12 +16,6 @@ export let teams = useList({ cacheKey: 'Teams', limit: 999, immediate: true, - transform(data) { - for (let team of data) { - team.name = team.name.toString() - } - return data - }, }) export let activeTeams = computed(() => { diff --git a/frontend/src/pages/Page.vue b/frontend/src/pages/Page.vue index d85704bf..793f55a9 100644 --- a/frontend/src/pages/Page.vue +++ b/frontend/src/pages/Page.vue @@ -165,6 +165,7 @@ const updateUrlSlug = () => { name: page.doc?.project ? 'SpacePage' : 'Page', params: { ...route.params, + spaceId: page.doc?.project, slug: page.doc?.slug, }, query: route.query, diff --git a/frontend/src/pages/SpaceList.vue b/frontend/src/pages/SpaceList.vue index 15f0387d..134479f2 100644 --- a/frontend/src/pages/SpaceList.vue +++ b/frontend/src/pages/SpaceList.vue @@ -1,18 +1,56 @@ diff --git a/frontend/src/components/DiscussionRow.vue b/frontend/src/components/DiscussionRow.vue index ae4d5bf5..a3358c29 100644 --- a/frontend/src/components/DiscussionRow.vue +++ b/frontend/src/components/DiscussionRow.vue @@ -45,7 +45,13 @@ {{ user.full_name }} - in {{ discussion.project_title }} + + in {{ discussion.project_title }} + +
@@ -75,11 +81,13 @@ import { Tooltip } from 'frappe-ui' import { dayjs } from '@/utils/dayjs' import UserAvatar from './UserAvatar.vue' import UserInfo from './UserInfo.vue' +import { useSpace } from '@/data/spaces' +import { Discussion } from '@/data/discussions' import LucidePin from '~icons/lucide/pin' const props = defineProps<{ - discussion: Object + discussion: Discussion index: number total: number showSpaceName: boolean @@ -96,6 +104,10 @@ function discussionTimestamp(d) { return dayjs(timestamp).format('D MMM YYYY') } +function isSpacePrivate(spaceId: string) { + return useSpace(spaceId).value?.is_private +} + function discussionTimestampDescription(d) { return [`First Post: ${dayjs(d.creation)}`, `Latest Post: ${dayjs(d.last_post_at)}`].join('\n') } diff --git a/frontend/src/data/discussions.ts b/frontend/src/data/discussions.ts index 994bae48..74b5dbf5 100644 --- a/frontend/src/data/discussions.ts +++ b/frontend/src/data/discussions.ts @@ -3,7 +3,8 @@ import { useDoc, useList } from 'frappe-ui/src/data-fetching' import { UseListOptions } from 'frappe-ui/src/data-fetching/useList/types' import { GPDiscussion } from '@/types/doctypes' -interface Discussion extends GPDiscussion { +export interface Discussion extends GPDiscussion { + project_title: string last_visit: string last_post_at: string unread: boolean @@ -11,17 +12,18 @@ interface Discussion extends GPDiscussion { export type UseDiscussionOptions = Pick< UseListOptions, - 'cacheKey' | 'filters' | 'limit' | 'orderBy' + 'cacheKey' | 'filters' | 'limit' | 'orderBy' | 'immediate' > export function useDiscussions(options: UseDiscussionOptions) { const discussions = useList({ url: '/api/v2/method/gameplan.gameplan.doctype.gp_discussion.api.get_discussions', doctype: 'GP Discussion', - cacheKey: ['Discussions', { ...options }], + cacheKey: options.cacheKey ? ['Discussions', options.cacheKey] : undefined, filters: options.filters, limit: options.limit || 50, orderBy: options.orderBy, + immediate: options.immediate ?? true, transform(data) { return data.map((d) => ({ ...d, diff --git a/frontend/src/pages/Discussions.vue b/frontend/src/pages/Discussions.vue index ceb90791..d456d4c8 100644 --- a/frontend/src/pages/Discussions.vue +++ b/frontend/src/pages/Discussions.vue @@ -27,7 +27,9 @@ diff --git a/frontend/src/pages/PersonProfileBookmarks.vue b/frontend/src/pages/PersonProfileBookmarks.vue index 61b304a8..ae50213d 100644 --- a/frontend/src/pages/PersonProfileBookmarks.vue +++ b/frontend/src/pages/PersonProfileBookmarks.vue @@ -1,22 +1,8 @@ - diff --git a/frontend/src/pages/PersonProfilePosts.vue b/frontend/src/pages/PersonProfilePosts.vue index 64652c5e..30bf485f 100644 --- a/frontend/src/pages/PersonProfilePosts.vue +++ b/frontend/src/pages/PersonProfilePosts.vue @@ -1,9 +1,6 @@ From 5ff05706d2759bb4a1a36d03799e24b0600f7f4c Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Thu, 9 Jan 2025 17:29:11 +0530 Subject: [PATCH 64/86] fix: Poll styling and anonymous polls --- frontend/src/components/CommentsArea.vue | 3 ++ frontend/src/components/Poll.vue | 40 +++++++++++++----------- 2 files changed, 24 insertions(+), 19 deletions(-) diff --git a/frontend/src/components/CommentsArea.vue b/frontend/src/components/CommentsArea.vue index 9e99f30e..4f53848e 100644 --- a/frontend/src/components/CommentsArea.vue +++ b/frontend/src/components/CommentsArea.vue @@ -173,6 +173,7 @@ const newComment = ref(localStorage.getItem(draftCommentKey()) || '') const newPoll = ref({ title: '', multiple_answers: false, + anonymous: false, options: [ { title: '', idx: 1 }, { title: '', idx: 2 }, @@ -303,6 +304,7 @@ function resetCommentState() { newPoll.value = { title: '', multiple_answers: false, + anonymouse: false, options: [ { title: '', idx: 1 }, { title: '', idx: 2 }, @@ -356,6 +358,7 @@ function submitPoll() { .submit({ discussion: props.name, title: newPoll.value.title, + anonymous: newPoll.value.anonymous ? 1 : 0, multiple_answers: newPoll.value.multiple_answers ? 1 : 0, options: newPoll.value.options, }) diff --git a/frontend/src/components/Poll.vue b/frontend/src/components/Poll.vue index d094894c..c5656df1 100644 --- a/frontend/src/components/Poll.vue +++ b/frontend/src/components/Poll.vue @@ -40,7 +40,7 @@ />
-
{{ _poll.title }}
+
{{ _poll.title }}
Multiple answers · Anonymous · @@ -72,7 +72,7 @@ />
-
{{ option.title }}
+
{{ option.title }}
({{ option.percentage }}%)
@@ -82,29 +82,31 @@
- + +
-
@@ -37,6 +31,7 @@ class="w-full border-0 p-0 pt-4 text-3xl font-semibold focus:outline-none focus:ring-0 bg-surface-white text-ink-gray-9" type="text" v-model="title" + @change="autosave" @keydown.enter="textEditor?.editor?.commands.focus()" ref="titleInput" /> @@ -44,7 +39,12 @@ import { ref, computed, onMounted, onBeforeUnmount, useTemplateRef } from 'vue' import { useRoute, useRouter } from 'vue-router' -import { Breadcrumbs, TextEditor, usePageMeta } from 'frappe-ui' +import { Breadcrumbs, TextEditor, usePageMeta, debounce } from 'frappe-ui' import { useDoc } from 'frappe-ui/src/data-fetching' import { useSpace } from '@/data/spaces' import { GPPage } from '@/types/doctypes' @@ -83,9 +83,9 @@ const page = useDoc({ name: () => props.pageId, }) -page.onSuccess(() => { - title.value = page.doc?.title || '' - content.value = page.doc?.content || '' +page.onSuccess((doc) => { + title.value = doc.title || '' + content.value = doc.content || '' updateUrlSlug() titleInput.value?.focus() }) @@ -111,6 +111,7 @@ const breadcrumbs = computed(() => { name: 'Page', params: { pageId: props.pageId, slug: props.slug }, }, + isPageTitle: true, }, ] } @@ -145,13 +146,30 @@ const breadcrumbs = computed(() => { ] }) +const isAutosaving = ref(false) +const MIN_AUTOSAVING_DURATION = 2000 // 2 seconds + const save = () => { - page.setValue.submit({ - title: title.value, - content: content.value, - }) + isAutosaving.value = true + const startTime = Date.now() + + page.setValue + .submit({ + title: title.value, + content: content.value, + }) + .finally(() => { + const elapsedTime = Date.now() - startTime + const remainingTime = Math.max(0, MIN_AUTOSAVING_DURATION - elapsedTime) + + setTimeout(() => { + isAutosaving.value = false + }, remainingTime) + }) } +const autosave = debounce(save, 1000) + const handleKeyboardShortcuts = (e: KeyboardEvent) => { if (e.key === 's' && (e.metaKey || e.ctrlKey)) { e.preventDefault() From 3a2a8997bd54f01da541423246f0598f4d299773 Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Thu, 9 Jan 2025 17:56:16 +0530 Subject: [PATCH 66/86] fix: fetch poll doc always --- frontend/src/components/Poll.vue | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/src/components/Poll.vue b/frontend/src/components/Poll.vue index c5656df1..09cdadb7 100644 --- a/frontend/src/components/Poll.vue +++ b/frontend/src/components/Poll.vue @@ -153,7 +153,6 @@ export default { type: 'document', doctype: 'GP Poll', name: this.poll.name, - auto: false, realtime: true, whitelistedMethods: { submitVote: 'submit_vote', From 8d278bf69f6f9a4a8ba26bf4a2d30201a7b6cf7d Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Thu, 9 Jan 2025 17:56:32 +0530 Subject: [PATCH 67/86] fix: update title on input --- frontend/src/pages/Page.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/pages/Page.vue b/frontend/src/pages/Page.vue index 45d09676..666d875c 100644 --- a/frontend/src/pages/Page.vue +++ b/frontend/src/pages/Page.vue @@ -31,7 +31,7 @@ class="w-full border-0 p-0 pt-4 text-3xl font-semibold focus:outline-none focus:ring-0 bg-surface-white text-ink-gray-9" type="text" v-model="title" - @change="autosave" + @input="autosave" @keydown.enter="textEditor?.editor?.commands.focus()" ref="titleInput" /> From 32c7931131b4586b76699f8be6265e570c071bab Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Thu, 9 Jan 2025 19:09:54 +0530 Subject: [PATCH 68/86] fix: delete comments instead of setting deleted_at --- frontend/src/components/Comment.vue | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/frontend/src/components/Comment.vue b/frontend/src/components/Comment.vue index 6f7b2dae..a430c71d 100644 --- a/frontend/src/components/Comment.vue +++ b/frontend/src/components/Comment.vue @@ -179,12 +179,7 @@ const dropdownOptions = computed(() => [ variant: 'solid', theme: 'red', onClick: ({ close }) => { - return props.comments.setValue - .submit({ - name: props.comment.name, - deleted_at: $dayjs().format('YYYY-MM-DD HH:mm:ss'), - }) - .then(close) + return props.comments.delete.submit({ name: props.comment.name }).then(close) }, }, ], From 135db51a42432fc0a293a418ceec26b60fd331bc Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Fri, 10 Jan 2025 14:47:51 +0530 Subject: [PATCH 69/86] fix: add container padding in discussion view --- frontend/src/components/DiscussionView.vue | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/DiscussionView.vue b/frontend/src/components/DiscussionView.vue index 80839c59..9006099a 100644 --- a/frontend/src/components/DiscussionView.vue +++ b/frontend/src/components/DiscussionView.vue @@ -1,6 +1,6 @@ + + diff --git a/frontend/src/pages/Page.vue b/frontend/src/pages/Page.vue index 666d875c..d7b4cb99 100644 --- a/frontend/src/pages/Page.vue +++ b/frontend/src/pages/Page.vue @@ -3,28 +3,55 @@
- - - - -
-
- Last updated {{ $dayjs(page.doc.modified).format('LLL') }} + Updated {{ $dayjs(page.doc.modified).format('lll') }}
{ }, ] } - - if (!space.value) return [] - return [ - { - label: 'Spaces', - route: { name: 'Spaces' }, - }, - { - label: space.value?.title, - route: { - name: 'Space', - params: { spaceId: space.value.name }, - }, - }, - { - label: 'Pages', - route: { - name: 'SpacePages', - params: { spaceId: space.value.name }, - }, - }, - { - label: pageTitle.value, - route: { - name: 'SpacePage', - params: { pageId: props.pageId, slug: props.slug, spaceId: space.value.name }, - }, - }, - ] }) const isAutosaving = ref(false) diff --git a/frontend/src/pages/Space.vue b/frontend/src/pages/Space.vue index d03dd527..df7df00f 100644 --- a/frontend/src/pages/Space.vue +++ b/frontend/src/pages/Space.vue @@ -2,24 +2,28 @@
- - +
+
+ +
Page not found
+
+
diff --git a/frontend/src/pages/OnboardingStepInvites.vue b/frontend/src/pages/OnboardingStepInvites.vue deleted file mode 100644 index d0b56284..00000000 --- a/frontend/src/pages/OnboardingStepInvites.vue +++ /dev/null @@ -1,37 +0,0 @@ - - diff --git a/frontend/src/pages/OnboardingStepProject.vue b/frontend/src/pages/OnboardingStepProject.vue deleted file mode 100644 index a2717c8e..00000000 --- a/frontend/src/pages/OnboardingStepProject.vue +++ /dev/null @@ -1,15 +0,0 @@ - - diff --git a/frontend/src/pages/OnboardingStepTeam.vue b/frontend/src/pages/OnboardingStepTeam.vue deleted file mode 100644 index a6a86df1..00000000 --- a/frontend/src/pages/OnboardingStepTeam.vue +++ /dev/null @@ -1,15 +0,0 @@ - - diff --git a/frontend/src/pages/SpaceList.vue b/frontend/src/pages/SpaceList.vue index 2cfc96f1..fe2d7558 100644 --- a/frontend/src/pages/SpaceList.vue +++ b/frontend/src/pages/SpaceList.vue @@ -29,12 +29,15 @@
-
{{ group.title || group.name }}
+
+ {{ noCategories ? 'All spaces' : group.title || group.name }} +
Date: Thu, 23 Jan 2025 14:02:04 +0530 Subject: [PATCH 81/86] fix: capture last_post in discussion - patch to set last_post in existing data - organize methods in discussion controller --- frontend/src/types/doctypes.ts | 6 +- .../gameplan/doctype/gp_comment/gp_comment.py | 37 +++---- .../gameplan/doctype/gp_discussion/api.py | 64 ++++++++++- .../doctype/gp_discussion/gp_discussion.js | 7 +- .../doctype/gp_discussion/gp_discussion.json | 16 ++- .../doctype/gp_discussion/gp_discussion.py | 100 ++++++++++++------ .../gp_discussion/patches/set_last_post.py | 25 +++++ gameplan/gameplan/doctype/gp_poll/gp_poll.py | 15 ++- gameplan/gameplan/doctype/gp_task/gp_task.py | 6 ++ gameplan/patches.txt | 3 +- 10 files changed, 222 insertions(+), 57 deletions(-) create mode 100644 gameplan/gameplan/doctype/gp_discussion/patches/set_last_post.py diff --git a/frontend/src/types/doctypes.ts b/frontend/src/types/doctypes.ts index 54213df6..b8ad4bc1 100644 --- a/frontend/src/types/doctypes.ts +++ b/frontend/src/types/doctypes.ts @@ -117,7 +117,7 @@ export interface GPNotification extends DocType { team?: string } -// Last updated: 2024-02-06 12:11:11.919002 +// Last updated: 2025-01-22 16:06:16.029266 export interface GPDiscussion extends DocType { /** Project: Link (GP Project) */ project: string @@ -149,6 +149,10 @@ export interface GPDiscussion extends DocType { pinned_at?: string /** Pinned By: Link (User) */ pinned_by?: string + /** Last Post Type: Select */ + last_post_type: 'GP Comment' | 'GP Poll' + /** Last Post: Dynamic Link (last_post_type) */ + last_post?: string } // Last updated: 2023-02-13 21:00:23.191195 diff --git a/gameplan/gameplan/doctype/gp_comment/gp_comment.py b/gameplan/gameplan/doctype/gp_comment/gp_comment.py index 6906db22..f502bffe 100644 --- a/gameplan/gameplan/doctype/gp_comment/gp_comment.py +++ b/gameplan/gameplan/doctype/gp_comment/gp_comment.py @@ -24,26 +24,27 @@ def before_insert(self): frappe.throw("Cannot add comment to a closed discussion") def after_insert(self): - if self.reference_doctype not in ["GP Discussion", "GP Task"]: + self.update_discussion_meta() + self.update_task_meta() + + def after_delete(self): + self.update_discussion_meta() + self.update_task_meta() + + def update_discussion_meta(self): + if self.reference_doctype != "GP Discussion": return - reference_doc = frappe.get_doc(self.reference_doctype, self.reference_name) - if reference_doc.meta.has_field("last_post_at"): - reference_doc.set("last_post_at", frappe.utils.now()) - if reference_doc.meta.has_field("last_post_by"): - reference_doc.set("last_post_by", frappe.session.user) - if reference_doc.meta.has_field("comments_count"): - reference_doc.set("comments_count", reference_doc.comments_count + 1) - if reference_doc.doctype == "GP Discussion": - reference_doc.update_participants_count() - reference_doc.track_visit() - reference_doc.save(ignore_permissions=True) - - def on_trash(self): - if self.reference_doctype not in ["GP Discussion", "GP Task"]: + discussion = frappe.get_doc("GP Discussion", self.reference_name) + discussion.update_last_post() + discussion.update_post_count() + discussion.update_participants_count() + discussion.track_visit() + discussion.save(ignore_permissions=True) + + def update_task_meta(self): + if self.reference_doctype != "GP Task": return - reference_doc = frappe.get_doc(self.reference_doctype, self.reference_name) - if reference_doc.meta.has_field("comments_count"): - reference_doc.db_set("comments_count", reference_doc.comments_count - 1) + frappe.get_doc("GP Task", self.reference_name).update_comments_count() def validate(self): self.content = remove_empty_trailing_paragraphs(self.content) diff --git a/gameplan/gameplan/doctype/gp_discussion/api.py b/gameplan/gameplan/doctype/gp_discussion/api.py index 3baa4d97..187a3bcb 100644 --- a/gameplan/gameplan/doctype/gp_discussion/api.py +++ b/gameplan/gameplan/doctype/gp_discussion/api.py @@ -1,8 +1,10 @@ # Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors # MIT License. See license.txt +from html import unescape import frappe +from bleach import clean from frappe.utils import cint from pypika.terms import ExistsCriterion @@ -79,6 +81,14 @@ def get_discussions(filters=None, order_by=None, start=None, limit=None): has_next_page = len(discussions) > limit discussions = discussions[:limit] + discussions = include_ongoing_polls(discussions) + discussions = include_last_post_content(discussions) + + frappe.response["has_next_page"] = has_next_page + return discussions + + +def include_ongoing_polls(discussions): Poll = frappe.qb.DocType("GP Poll") discussion_names = [d.name for d in discussions] ongoing_polls = ( @@ -95,8 +105,47 @@ def get_discussions(filters=None, order_by=None, start=None, limit=None): ) for discussion in discussions: discussion["ongoing_polls"] = [p for p in ongoing_polls if str(p.discussion) == str(discussion.name)] + return discussions + + +def include_last_post_content(discussions): + Comment = frappe.qb.DocType("GP Comment") + + last_comments_name = [d.last_post for d in discussions if d.last_post_type == "GP Comment"] + last_comments_content = ( + frappe.qb.from_(Comment) + .select(Comment.content, Comment.name) + .where(Comment.name.isin(last_comments_name)) + .run(as_dict=1) + if last_comments_name + else [] + ) + last_comments_content_map = {c.name: c.content for c in last_comments_content} + + Poll = frappe.qb.DocType("GP Poll") + last_polls_name = [d.last_post for d in discussions if d.last_post_type == "GP Poll"] + last_poll_title = ( + frappe.qb.from_(Poll) + .select(Poll.name, Poll.title) + .where(Poll.name.isin(last_polls_name)) + .run(as_dict=1) + if last_polls_name + else [] + ) + last_poll_title_map = {p.name: p.title for p in last_poll_title} + + for discussion in discussions: + if discussion.last_post_type == "GP Comment": + discussion.last_comment_content = html_to_text_preview( + last_comments_content_map.get(cint(discussion.last_post)) + ) + if discussion.last_post_type == "GP Poll": + discussion.last_poll_title = last_poll_title_map.get(cint(discussion.last_post)) + + if not discussion.last_post: + discussion.last_post_by = discussion.owner + discussion.last_comment_content = html_to_text_preview(discussion.content) - frappe.response["has_next_page"] = has_next_page return discussions @@ -120,3 +169,16 @@ def clause_discussions_bookmarked_by_user(user): Bookmark = frappe.qb.DocType("GP Bookmark") bookmarked_discussions = Bookmark.select(Bookmark.discussion).where(Bookmark.user == user) return Discussion.name.isin(bookmarked_discussions) + + +def html_to_text_preview(html): + """Convert HTML to text preview of 100 characters""" + + length = 50 + text = clean(html or "", tags=[], strip=True) + text = unescape(text) + text = text.strip()[:length] + if len(text) == length: + text += "..." + + return text diff --git a/gameplan/gameplan/doctype/gp_discussion/gp_discussion.js b/gameplan/gameplan/doctype/gp_discussion/gp_discussion.js index 0a3818aa..38d03591 100644 --- a/gameplan/gameplan/doctype/gp_discussion/gp_discussion.js +++ b/gameplan/gameplan/doctype/gp_discussion/gp_discussion.js @@ -2,6 +2,9 @@ // For license information, please see license.txt frappe.ui.form.on("GP Discussion", { - // refresh: function(frm) { - // } + refresh: function (frm) { + frm.add_custom_button(__("Show in Gameplan"), function () { + window.open(`/g/space/${frm.doc.project}/discussion/${frm.doc.name}`); + }); + }, }); diff --git a/gameplan/gameplan/doctype/gp_discussion/gp_discussion.json b/gameplan/gameplan/doctype/gp_discussion/gp_discussion.json index fa380ae7..3a22ccbf 100644 --- a/gameplan/gameplan/doctype/gp_discussion/gp_discussion.json +++ b/gameplan/gameplan/doctype/gp_discussion/gp_discussion.json @@ -14,6 +14,8 @@ "content", "reactions", "last_post_at", + "last_post_type", + "last_post", "last_post_by", "closed_at", "closed_by", @@ -115,11 +117,23 @@ "fieldtype": "Link", "label": "Pinned By", "options": "User" + }, + { + "fieldname": "last_post_type", + "fieldtype": "Select", + "label": "Last Post Type", + "options": "\nGP Comment\nGP Poll" + }, + { + "fieldname": "last_post", + "fieldtype": "Dynamic Link", + "label": "Last Post", + "options": "last_post_type" } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2024-02-06 12:11:11.919002", + "modified": "2025-01-22 16:06:16.029266", "modified_by": "Administrator", "module": "Gameplan", "name": "GP Discussion", diff --git a/gameplan/gameplan/doctype/gp_discussion/gp_discussion.py b/gameplan/gameplan/doctype/gp_discussion/gp_discussion.py index b39dbdf7..2b98e633 100644 --- a/gameplan/gameplan/doctype/gp_discussion/gp_discussion.py +++ b/gameplan/gameplan/doctype/gp_discussion/gp_discussion.py @@ -14,6 +14,7 @@ class GPDiscussion(HasActivity, HasMentions, HasReactions, Document): + # Class Configuration on_delete_cascade = ["GP Comment", "GP Discussion Visit", "GP Activity", "GP Poll"] on_delete_set_null = ["GP Notification"] activities = [ @@ -25,6 +26,7 @@ class GPDiscussion(HasActivity, HasMentions, HasReactions, Document): ] mentions_field = "content" + # Lifecycle Methods def as_dict(self, *args, **kwargs): d = super(__class__, self).as_dict(*args, **kwargs) last_visit = frappe.db.get_value( @@ -84,36 +86,7 @@ def on_update(self): def before_save(self): self.update_slug() - def update_slug(self): - self.slug = url_safe_slug(self.title) - - def log_title_update(self): - if self.has_value_changed("title") and self.get_doc_before_save(): - self.log_activity( - "Discussion Title Changed", - data={"old_title": self.get_doc_before_save().title, "new_title": self.title}, - ) - - def update_search_index(self): - if self.has_value_changed("title") or self.has_value_changed("content"): - search = GameplanSearch() - search.index_doc(self) - - def update_participants_count(self): - participants = frappe.db.get_all( - "GP Comment", - filters={"reference_doctype": self.doctype, "reference_name": self.name}, - pluck="owner", - ) - participants += frappe.db.get_all("GP Poll", filters={"discussion": self.name}, pluck="owner") - participants.append(self.owner) - self.participants_count = len(list(set(participants))) - - def check_if_project_is_archived(self): - project_name, archived_at = frappe.db.get_value("GP Project", self.project, ["name", "archived_at"]) - if archived_at: - frappe.throw(f"Project {project_name} is archived. Cannot create discussions.") - + # Whitelisted Methods @frappe.whitelist() def track_visit(self): if frappe.flags.read_only: @@ -194,8 +167,75 @@ def remove_bookmark(self): return frappe.get_doc("GP Bookmark", bookmark).delete() + # Utility Methods def is_bookmarked(self): return bool(frappe.db.exists("GP Bookmark", {"discussion": self.name, "user": frappe.session.user})) + def update_slug(self): + self.slug = url_safe_slug(self.title) + + def update_search_index(self): + if self.has_value_changed("title") or self.has_value_changed("content"): + search = GameplanSearch() + search.index_doc(self) + + def update_participants_count(self): + participants = frappe.db.get_all( + "GP Comment", + filters={"reference_doctype": self.doctype, "reference_name": self.name}, + pluck="owner", + ) + participants += frappe.db.get_all("GP Poll", filters={"discussion": self.name}, pluck="owner") + participants.append(self.owner) + self.participants_count = len(list(set(participants))) + + def update_last_post(self): + def get_last_post(doctype, filters): + return frappe.db.get_value( + doctype, + filters, + ["name", "creation", "owner"], + order_by="creation desc", + as_dict=True, + ) + + def update_last_post_fields(post_type, post_data): + self.last_post_type = post_type + self.last_post = post_data.name + self.last_post_at = post_data.creation + self.last_post_by = post_data.owner + + last_comment = get_last_post( + "GP Comment", {"reference_doctype": self.doctype, "reference_name": self.name} + ) + last_poll = get_last_post("GP Poll", {"discussion": self.name}) + + if last_comment and last_poll: + latest = last_comment if last_comment.creation > last_poll.creation else last_poll + update_last_post_fields("GP Comment" if latest == last_comment else "GP Poll", latest) + elif last_comment: + update_last_post_fields("GP Comment", last_comment) + elif last_poll: + update_last_post_fields("GP Poll", last_poll) + + def update_post_count(self): + comments_count = frappe.db.count( + "GP Comment", {"reference_doctype": self.doctype, "reference_name": self.name} + ) + polls_count = frappe.db.count("GP Poll", {"discussion": self.name}) + self.comments_count = comments_count + polls_count + def update_discussions_count(self): frappe.get_doc("GP Project", self.project).update_discussions_count() + + def log_title_update(self): + if self.has_value_changed("title") and self.get_doc_before_save(): + self.log_activity( + "Discussion Title Changed", + data={"old_title": self.get_doc_before_save().title, "new_title": self.title}, + ) + + def check_if_project_is_archived(self): + project_name, archived_at = frappe.db.get_value("GP Project", self.project, ["name", "archived_at"]) + if archived_at: + frappe.throw(f"Project {project_name} is archived. Cannot create discussions.") diff --git a/gameplan/gameplan/doctype/gp_discussion/patches/set_last_post.py b/gameplan/gameplan/doctype/gp_discussion/patches/set_last_post.py new file mode 100644 index 00000000..9d51c702 --- /dev/null +++ b/gameplan/gameplan/doctype/gp_discussion/patches/set_last_post.py @@ -0,0 +1,25 @@ +# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and Contributors +import frappe + + +def execute(): + frappe.db.sql(""" + UPDATE `tabGP Discussion` d + INNER JOIN ( + SELECT discussion, creation, owner, name, type, + ROW_NUMBER() OVER (PARTITION BY discussion ORDER BY creation DESC) as rn + FROM ( + SELECT reference_name as discussion, creation, owner, name, 'GP Comment' as type + FROM `tabGP Comment` + WHERE reference_doctype = 'GP Discussion' + UNION ALL + SELECT discussion, creation, owner, name, 'GP Poll' as type + FROM `tabGP Poll` + ) combined_posts + ) p ON d.name = p.discussion AND p.rn = 1 + SET + d.last_post_type = p.type, + d.last_post = p.name, + d.last_post_at = p.creation, + d.last_post_by = p.owner + """) diff --git a/gameplan/gameplan/doctype/gp_poll/gp_poll.py b/gameplan/gameplan/doctype/gp_poll/gp_poll.py index ac84ecd3..393b816e 100644 --- a/gameplan/gameplan/doctype/gp_poll/gp_poll.py +++ b/gameplan/gameplan/doctype/gp_poll/gp_poll.py @@ -9,6 +9,8 @@ class GPPoll(Document, GPPollAttributes): + on_delete_set_null = ["GP Discussion"] + def before_insert(self): self.options = [d for d in self.options if d.title] for option in self.options: @@ -22,10 +24,17 @@ def validate(self): self.total_votes = len(self.votes) def after_insert(self): + self.update_discussion_meta() + + def after_delete(self): + self.update_discussion_meta() + + def update_discussion_meta(self): + if not self.discussion: + return discussion = frappe.get_doc("GP Discussion", self.discussion) - discussion.last_post_at = frappe.utils.now() - discussion.last_post_by = frappe.session.user - discussion.comments_count = discussion.comments_count + 1 + discussion.update_last_post() + discussion.update_post_count() discussion.update_participants_count() discussion.track_visit() discussion.save(ignore_permissions=True) diff --git a/gameplan/gameplan/doctype/gp_task/gp_task.py b/gameplan/gameplan/doctype/gp_task/gp_task.py index 0efab3c9..ff34c1bc 100644 --- a/gameplan/gameplan/doctype/gp_task/gp_task.py +++ b/gameplan/gameplan/doctype/gp_task/gp_task.py @@ -49,6 +49,12 @@ def update_search_index(self): search = GameplanSearch() search.index_doc(self) + def update_comments_count(self): + comments_count = frappe.db.count( + "GP Comment", {"reference_doctype": "GP Task", "reference_name": self.name} + ) + self.db_set("comments_count", comments_count) + def on_trash(self): self.update_tasks_count() search = GameplanSearch() diff --git a/gameplan/patches.txt b/gameplan/patches.txt index ce2c5507..75948504 100644 --- a/gameplan/patches.txt +++ b/gameplan/patches.txt @@ -23,4 +23,5 @@ gameplan.gameplan.doctype.team_user_profile.patches.setup_rembg gameplan.gameplan.doctype.team_user_profile.patches.set_image gameplan.gameplan.doctype.gp_task.patches.set_status gameplan.gameplan.doctype.gp_discussion_visit.patches.add_unique_constraint -gameplan.gameplan.doctype.gp_project.patches.migrate_members_from_team \ No newline at end of file +gameplan.gameplan.doctype.gp_project.patches.migrate_members_from_team +gameplan.gameplan.doctype.gp_discussion.patches.set_last_post \ No newline at end of file From eae82815761e2b395e056599d9e9d5e831435551 Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Thu, 23 Jan 2025 14:31:00 +0530 Subject: [PATCH 82/86] fix: show last post content in discussion row --- frontend/src/components/CommentsList.vue | 3 +- frontend/src/components/DiscussionRow.vue | 65 ++++++++++++++--------- frontend/src/data/discussions.ts | 2 + 3 files changed, 42 insertions(+), 28 deletions(-) diff --git a/frontend/src/components/CommentsList.vue b/frontend/src/components/CommentsList.vue index 6eb0a017..d2b47689 100644 --- a/frontend/src/components/CommentsList.vue +++ b/frontend/src/components/CommentsList.vue @@ -34,7 +34,6 @@
-
+
- +
{{ discussionTimestamp(discussion) }}
From 1486ace48575258fa6e3633860abbee31c2564df Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Fri, 24 Jan 2025 16:25:45 +0530 Subject: [PATCH 84/86] fix: scroll to top button --- frontend/src/components/DiscussionView.vue | 12 +++++++++ frontend/src/utils/scrollContainer.js | 31 ++++++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/frontend/src/components/DiscussionView.vue b/frontend/src/components/DiscussionView.vue index 5cab1654..bcc6fbd2 100644 --- a/frontend/src/components/DiscussionView.vue +++ b/frontend/src/components/DiscussionView.vue @@ -189,6 +189,14 @@ />
+
+ +
@@ -208,6 +216,9 @@ import { useSpace } from '@/data/spaces' import { useGroupedSpaceOptions } from '@/data/groupedSpaces' import { useDiscussion } from '@/data/discussions' import { createDialog } from '@/utils/dialogs' +import { useScrollPosition } from '@/utils/scrollContainer' + +import LucideArrowUp from '~icons/lucide/arrow-up' const props = defineProps<{ postId: string @@ -216,6 +227,7 @@ const props = defineProps<{ const router = useRouter() const route = useRoute() +const { isScrolled, scrollToTop } = useScrollPosition() const editingPost = ref(false) const discussionMoveDialog = reactive<{ diff --git a/frontend/src/utils/scrollContainer.js b/frontend/src/utils/scrollContainer.js index d3170ab2..852d9f2f 100644 --- a/frontend/src/utils/scrollContainer.js +++ b/frontend/src/utils/scrollContainer.js @@ -1,3 +1,5 @@ +import { ref, onMounted, onBeforeUnmount } from 'vue' + export function scrollTo(...options) { if (!options || options.length === 0) return const container = getScrollContainer() @@ -9,3 +11,32 @@ export function getScrollContainer() { // window.scrollContainer is reference to the scroll container in DesktopLayout.vue and MobileLayout.vue return window.scrollContainer } + +export function useScrollPosition(options = { threshold: 200 }) { + const isScrolled = ref(false) + + function updateScrollPosition() { + const scrollContainer = getScrollContainer() + isScrolled.value = scrollContainer.scrollTop > options.threshold + } + + onMounted(() => { + const scrollContainer = getScrollContainer() + scrollContainer.addEventListener('scroll', updateScrollPosition) + }) + + onBeforeUnmount(() => { + const scrollContainer = getScrollContainer() + scrollContainer.removeEventListener('scroll', updateScrollPosition) + }) + + function scrollToTop() { + const scrollContainer = getScrollContainer() + scrollContainer.scrollTo({ top: 0, behavior: 'smooth' }) + } + + return { + isScrolled, + scrollToTop, + } +} From 855b7fd023706cc5757e5b8b6a957c0e2ff03ad1 Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Fri, 24 Jan 2025 16:26:04 +0530 Subject: [PATCH 85/86] fix: show views in discussion --- frontend/src/components/DiscussionView.vue | 11 ++++------- frontend/src/data/discussions.ts | 1 + .../gameplan/doctype/gp_discussion/gp_discussion.py | 1 + 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/frontend/src/components/DiscussionView.vue b/frontend/src/components/DiscussionView.vue index bcc6fbd2..9f11cf0d 100644 --- a/frontend/src/components/DiscussionView.vue +++ b/frontend/src/components/DiscussionView.vue @@ -73,13 +73,6 @@
- - {{ space?.title }} - - · {{ discussion.doc.participants_count == 1 @@ -87,6 +80,10 @@ : `${discussion.doc.participants_count} participants` }} +
) { last_unread_comment: string last_unread_poll: string is_bookmarked: boolean + views: number } interface DiscussionMethods { diff --git a/gameplan/gameplan/doctype/gp_discussion/gp_discussion.py b/gameplan/gameplan/doctype/gp_discussion/gp_discussion.py index 2b98e633..52fcdc92 100644 --- a/gameplan/gameplan/doctype/gp_discussion/gp_discussion.py +++ b/gameplan/gameplan/doctype/gp_discussion/gp_discussion.py @@ -57,6 +57,7 @@ def as_dict(self, *args, **kwargs): ) d.last_unread_poll = polls[0] if polls else None d.is_bookmarked = self.is_bookmarked() + d.views = frappe.db.count("GP Discussion Visit", {"discussion": self.name}) return d def before_insert(self): From 4fb4b42862e22f20071842b9d853a2f57fbd5687 Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Fri, 24 Jan 2025 16:44:55 +0530 Subject: [PATCH 86/86] fix: scroll to top for mobile - hide scroll to top button - scroll to top when the title is clicked --- frontend/src/components/DiscussionView.vue | 3 ++- frontend/src/components/SpaceBreadcrumbs.vue | 8 +++++++- frontend/src/pages/SpaceDiscussion.vue | 6 +++++- frontend/src/utils/composables.ts | 7 ++++++- frontend/src/utils/scrollContainer.js | 10 +++++----- 5 files changed, 25 insertions(+), 9 deletions(-) diff --git a/frontend/src/components/DiscussionView.vue b/frontend/src/components/DiscussionView.vue index 9f11cf0d..3abd5ebd 100644 --- a/frontend/src/components/DiscussionView.vue +++ b/frontend/src/components/DiscussionView.vue @@ -186,7 +186,7 @@ />
-
+