diff --git a/apiserver/plane/db/migrations/0046_alter_analyticview_created_by_and_more.py b/apiserver/plane/db/migrations/0046_alter_analyticview_created_by_and_more.py index 51082d14c..ae5753e07 100644 --- a/apiserver/plane/db/migrations/0046_alter_analyticview_created_by_and_more.py +++ b/apiserver/plane/db/migrations/0046_alter_analyticview_created_by_and_more.py @@ -4,6 +4,7 @@ from django.conf import settings from django.db import migrations, models import django.db.models.deletion import plane.db.models.issue +import uuid class Migration(migrations.Migration): @@ -12,6 +13,26 @@ class Migration(migrations.Migration): ] operations = [ + migrations.CreateModel( + name="issue_mentions", + fields=[ + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), + ('id', models.UUIDField(db_index=True, default=uuid.uuid4,editable=False, primary_key=True, serialize=False, unique=True)), + ('mention', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='issue_mention', to=settings.AUTH_USER_MODEL)), + ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL,related_name='issuemention_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')), + ('issue', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='issue_mention', to='db.issue')), + ('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_issuemention', to='db.project')), + ('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='issuemention_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')), + ('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_issuemention', to='db.workspace')), + ], + options={ + 'verbose_name': 'IssueMention', + 'verbose_name_plural': 'IssueMentions', + 'db_table': 'issue_mentions', + 'ordering': ('-created_at',), + }, + ), migrations.AlterField( model_name='issueproperty', name='properties', diff --git a/apiserver/plane/db/migrations/0047_issue_mention_and_more.py b/apiserver/plane/db/migrations/0047_issue_mention_and_more.py deleted file mode 100644 index 4300f1eb3..000000000 --- a/apiserver/plane/db/migrations/0047_issue_mention_and_more.py +++ /dev/null @@ -1,45 +0,0 @@ -# Generated by Django 4.2.5 on 2023-10-25 05:01 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion -import uuid - - -class Migration(migrations.Migration): - - dependencies = [ - ('db', '0046_alter_analyticview_created_by_and_more'), - ] - - operations = [ - migrations.CreateModel( - name="issue_mentions", - fields=[ - ('created_at', models.DateTimeField( - auto_now_add=True, verbose_name='Created At')), - ('updated_at', models.DateTimeField( - auto_now=True, verbose_name='Last Modified At')), - ('id', models.UUIDField(db_index=True, default=uuid.uuid4, - editable=False, primary_key=True, serialize=False, unique=True)), - ('mention', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, - related_name='issue_mention', to=settings.AUTH_USER_MODEL)), - ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, - related_name='issuemention_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')), - ('issue', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, - related_name='issue_mention', to='db.issue')), - ('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, - related_name='project_issuemention', to='db.project')), - ('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, - related_name='issuemention_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')), - ('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, - related_name='workspace_issuemention', to='db.workspace')), - ], - options={ - 'verbose_name': 'IssueMention', - 'verbose_name_plural': 'IssueMentions', - 'db_table': 'issue_mentions', - 'ordering': ('-created_at',), - }, - ) - ] diff --git a/web/components/issues/issue-layouts/filters/header/display-filters/display-filters-selection.tsx b/web/components/issues/issue-layouts/filters/header/display-filters/display-filters-selection.tsx index 09173fec8..0133f9015 100644 --- a/web/components/issues/issue-layouts/filters/header/display-filters/display-filters-selection.tsx +++ b/web/components/issues/issue-layouts/filters/header/display-filters/display-filters-selection.tsx @@ -47,8 +47,7 @@ export const DisplayFiltersSelection: React.FC = observer((props) => { {isDisplayFilterEnabled("group_by") && (
handleDisplayFiltersUpdate({ @@ -65,8 +64,7 @@ export const DisplayFiltersSelection: React.FC = observer((props) => { displayFilters.layout === "kanban" && (
handleDisplayFiltersUpdate({ sub_group_by: val, diff --git a/web/components/issues/issue-layouts/filters/header/display-filters/group-by.tsx b/web/components/issues/issue-layouts/filters/header/display-filters/group-by.tsx index b74fe2761..aa057e417 100644 --- a/web/components/issues/issue-layouts/filters/header/display-filters/group-by.tsx +++ b/web/components/issues/issue-layouts/filters/header/display-filters/group-by.tsx @@ -4,23 +4,23 @@ import { observer } from "mobx-react-lite"; // components import { FilterHeader, FilterOption } from "components/issues"; // types -import { TIssueGroupByOptions } from "types"; +import { IIssueDisplayFilterOptions, TIssueGroupByOptions } from "types"; // constants import { ISSUE_GROUP_BY_OPTIONS } from "constants/issue"; type Props = { - selectedGroupBy: TIssueGroupByOptions | undefined; - selectedSubGroupBy: TIssueGroupByOptions | undefined; + displayFilters: IIssueDisplayFilterOptions; groupByOptions: TIssueGroupByOptions[]; handleUpdate: (val: TIssueGroupByOptions) => void; }; export const FilterGroupBy: React.FC = observer((props) => { - const { selectedGroupBy, selectedSubGroupBy, groupByOptions, handleUpdate } = props; + const { displayFilters, groupByOptions, handleUpdate } = props; const [previewEnabled, setPreviewEnabled] = useState(true); - const activeGroupBy = selectedGroupBy ?? null; + const selectedGroupBy = displayFilters.group_by ?? null; + const selectedSubGroupBy = displayFilters.sub_group_by ?? null; return ( <> @@ -32,12 +32,13 @@ export const FilterGroupBy: React.FC = observer((props) => { {previewEnabled && (
{ISSUE_GROUP_BY_OPTIONS.filter((option) => groupByOptions.includes(option.key)).map((groupBy) => { - if (selectedSubGroupBy !== null && groupBy.key === selectedSubGroupBy) return null; + if (displayFilters.layout === "kanban" && selectedSubGroupBy !== null && groupBy.key === selectedSubGroupBy) + return null; return ( handleUpdate(groupBy.key)} title={groupBy.title} multiple={false} diff --git a/web/components/issues/issue-layouts/filters/header/display-filters/sub-group-by.tsx b/web/components/issues/issue-layouts/filters/header/display-filters/sub-group-by.tsx index 83f09092d..f66422427 100644 --- a/web/components/issues/issue-layouts/filters/header/display-filters/sub-group-by.tsx +++ b/web/components/issues/issue-layouts/filters/header/display-filters/sub-group-by.tsx @@ -4,22 +4,24 @@ import { observer } from "mobx-react-lite"; // components import { FilterHeader, FilterOption } from "components/issues"; // types -import { TIssueGroupByOptions } from "types"; +import { IIssueDisplayFilterOptions, TIssueGroupByOptions } from "types"; // constants import { ISSUE_GROUP_BY_OPTIONS } from "constants/issue"; type Props = { - selectedGroupBy: TIssueGroupByOptions | undefined; - selectedSubGroupBy: TIssueGroupByOptions | undefined; + displayFilters: IIssueDisplayFilterOptions; handleUpdate: (val: TIssueGroupByOptions) => void; subGroupByOptions: TIssueGroupByOptions[]; }; export const FilterSubGroupBy: React.FC = observer((props) => { - const { selectedGroupBy, selectedSubGroupBy, handleUpdate, subGroupByOptions } = props; + const { displayFilters, handleUpdate, subGroupByOptions } = props; const [previewEnabled, setPreviewEnabled] = useState(true); + const selectedGroupBy = displayFilters.group_by ?? null; + const selectedSubGroupBy = displayFilters.sub_group_by ?? null; + return ( <> = observer((props) => { return (
- {/* TODO: have to implement */} - {group_by && group_by === "projects" && ( + {group_by && group_by === "project" && ( = observer((props) => { )}
+ {sub_group_by && sub_group_by === "project" && ( + + )} + {sub_group_by && sub_group_by === "state" && ( { - const { user: userStore } = useMobxStore(); + const { + user: { fetchCurrentUser, fetchCurrentUserSettings }, + } = useMobxStore(); // router const router = useRouter(); const { next: next_url } = router.query as { next: string }; - // states const [isLoading, setLoading] = useState(false); // toast @@ -44,44 +45,44 @@ export const SignInView = observer(() => { data && (data?.email_password_login || !(data?.email_password_login || data?.magic_login || data?.google || data?.github)); - useEffect(() => { - userStore.fetchCurrentUserSettings().then((settings) => { - setLoading(true); - if (next_url) router.push(next_url); - else - router.push( - `/${ - settings.workspace.last_workspace_slug - ? settings.workspace.last_workspace_slug - : settings.workspace.fallback_workspace_slug - }` - ); - }); - }, [userStore, router, next_url]); - - const handleLoginRedirection = () => { - userStore.fetchCurrentUser().then((user) => { - const isOnboarded = user.is_onboarded; - - if (isOnboarded) { - userStore - .fetchCurrentUserSettings() - .then((userSettings: IUserSettings) => { - const workspaceSlug = - userSettings?.workspace?.last_workspace_slug || userSettings?.workspace?.fallback_workspace_slug; - if (next_url) router.push(next_url); - else if (workspaceSlug) router.push(`/${workspaceSlug}`); - else if (userSettings.workspace.invites > 0) router.push("/invitations"); - else router.push("/create-workspace"); - }) - .catch(() => { - setLoading(false); - }); - } else { + const handleLoginRedirection = useCallback( + (user: IUser) => { + // if the user is not onboarded, redirect them to the onboarding page + if (!user.is_onboarded) { router.push("/onboarding"); + return; } + // if next_url is provided, redirect the user to that url + if (next_url) { + router.push(next_url); + return; + } + + // if the user is onboarded, fetch their last workspace details + fetchCurrentUserSettings() + .then((userSettings: IUserSettings) => { + const workspaceSlug = + userSettings?.workspace?.last_workspace_slug || userSettings?.workspace?.fallback_workspace_slug; + if (workspaceSlug) router.push(`/${workspaceSlug}`); + else if (userSettings.workspace.invites > 0) router.push("/invitations"); + else router.push("/create-workspace"); + }) + .catch(() => { + setLoading(false); + }); + }, + [fetchCurrentUserSettings, router, next_url] + ); + + const mutateUserInfo = useCallback(() => { + fetchCurrentUser().then((user) => { + handleLoginRedirection(user); }); - }; + }, [fetchCurrentUser, handleLoginRedirection]); + + useEffect(() => { + mutateUserInfo(); + }, [mutateUserInfo]); const handleGoogleSignIn = async ({ clientId, credential }: any) => { try { @@ -94,7 +95,7 @@ export const SignInView = observer(() => { }; const response = await authService.socialAuth(socialAuthPayload); if (response) { - handleLoginRedirection(); + mutateUserInfo(); } } else { setLoading(false); @@ -121,7 +122,7 @@ export const SignInView = observer(() => { }; const response = await authService.socialAuth(socialAuthPayload); if (response) { - handleLoginRedirection(); + mutateUserInfo(); } } else { setLoading(false); @@ -142,13 +143,7 @@ export const SignInView = observer(() => { return authService .emailLogin(formData) .then(() => { - userStore.fetchCurrentUser().then((user) => { - const isOnboard = user.onboarding_step.profile_complete; - if (isOnboard) handleLoginRedirection(); - else { - router.push("/onboarding"); - } - }); + mutateUserInfo(); }) .catch((err) => { setLoading(false); @@ -164,7 +159,7 @@ export const SignInView = observer(() => { try { setLoading(true); if (response) { - handleLoginRedirection(); + mutateUserInfo(); } } catch (err: any) { setLoading(false); diff --git a/web/components/workspace/create-workspace-form.tsx b/web/components/workspace/create-workspace-form.tsx index b127a077b..41251dac6 100644 --- a/web/components/workspace/create-workspace-form.tsx +++ b/web/components/workspace/create-workspace-form.tsx @@ -13,7 +13,7 @@ import { Button, CustomSelect, Input } from "@plane/ui"; // types import { IWorkspace } from "types"; // constants -import { ORGANIZATION_SIZE } from "constants/workspace"; +import { ORGANIZATION_SIZE, RESTRICTED_URLS } from "constants/workspace"; type Props = { onSubmit?: (res: IWorkspace) => Promise; @@ -30,22 +30,6 @@ type Props = { }; }; -const restrictedUrls = [ - "api", - "installations", - "404", - "create-workspace", - "error", - "invitations", - "magic-sign-in", - "onboarding", - "profile", - "reset-password", - "sign-up", - "spaces", - "workspace-member-invitation", -]; - const workspaceService = new WorkspaceService(); export const CreateWorkspaceForm: FC = observer((props) => { @@ -81,7 +65,7 @@ export const CreateWorkspaceForm: FC = observer((props) => { await workspaceService .workspaceSlugCheck(formData.slug) .then(async (res) => { - if (res.status === true && !restrictedUrls.includes(formData.slug)) { + if (res.status === true && !RESTRICTED_URLS.includes(formData.slug)) { setSlugError(false); await workspaceStore @@ -141,7 +125,6 @@ export const CreateWorkspaceForm: FC = observer((props) => { render={({ field: { value, ref, onChange } }) => ( { @@ -167,15 +150,15 @@ export const CreateWorkspaceForm: FC = observer((props) => { rules={{ required: "Workspace URL is required", }} - render={({ field: { value, ref } }) => ( + render={({ field: { onChange, value, ref } }) => ( - /^[a-zA-Z0-9_-]+$/.test(e.target.value) ? setInvalidSlug(false) : setInvalidSlug(true) - } + onChange={(e) => { + /^[a-zA-Z0-9_-]+$/.test(e.target.value) ? setInvalidSlug(false) : setInvalidSlug(true); + onChange(e.target.value.toLowerCase()); + }} ref={ref} hasError={Boolean(errors.slug)} placeholder="Enter workspace name..." diff --git a/web/components/workspace/sidebar-dropdown.tsx b/web/components/workspace/sidebar-dropdown.tsx index 873a12e47..75a57143b 100644 --- a/web/components/workspace/sidebar-dropdown.tsx +++ b/web/components/workspace/sidebar-dropdown.tsx @@ -3,7 +3,6 @@ import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; import Link from "next/link"; import { Menu, Transition } from "@headlessui/react"; -import { useTheme } from "next-themes"; import { Check, LogOut, Plus, Settings, UserCircle2 } from "lucide-react"; // mobx store import { useMobxStore } from "lib/mobx/store-provider"; @@ -55,8 +54,6 @@ export const WorkspaceSidebarDropdown = observer(() => { const { workspaces, currentWorkspace: activeWorkspace } = workspaceStore; const user = userStore.currentUser; - const { setTheme } = useTheme(); - const { setToastAlert } = useToast(); const handleWorkspaceNavigation = (workspace: IWorkspace) => { @@ -81,7 +78,6 @@ export const WorkspaceSidebarDropdown = observer(() => { .signOut() .then(() => { router.push("/"); - setTheme("system"); }) .catch(() => setToastAlert({ diff --git a/web/constants/workspace.ts b/web/constants/workspace.ts index 8f55fac4c..084bdc68a 100644 --- a/web/constants/workspace.ts +++ b/web/constants/workspace.ts @@ -90,3 +90,19 @@ export const DEFAULT_GLOBAL_VIEWS_LIST: { label: "Subscribed", }, ]; + +export const RESTRICTED_URLS = [ + "api", + "installations", + "404", + "create-workspace", + "error", + "invitations", + "magic-sign-in", + "onboarding", + "profile", + "reset-password", + "sign-up", + "spaces", + "workspace-member-invitation", +]; diff --git a/web/hooks/use-user-auth.tsx b/web/hooks/use-user-auth.tsx index b6de6b964..0f555b9dc 100644 --- a/web/hooks/use-user-auth.tsx +++ b/web/hooks/use-user-auth.tsx @@ -9,7 +9,7 @@ import { CURRENT_USER } from "constants/fetch-keys"; import { UserService } from "services/user.service"; import { WorkspaceService } from "services/workspace.service"; // types -import type { IWorkspace, IUser } from "types"; +import type { IUser } from "types"; const userService = new UserService(); const workspaceService = new WorkspaceService(); @@ -33,9 +33,7 @@ const useUserAuth = (routeAuth: "sign-in" | "onboarding" | "admin" | null = "adm useEffect(() => { const handleWorkSpaceRedirection = async () => { workspaceService.userWorkspaces().then(async (userWorkspaces) => { - const lastActiveWorkspace = userWorkspaces.find( - (workspace: IWorkspace) => workspace.id === user?.last_workspace_id - ); + const lastActiveWorkspace = userWorkspaces.find((workspace) => workspace.id === user?.last_workspace_id); if (lastActiveWorkspace) { router.push(`/${lastActiveWorkspace.slug}`); return; diff --git a/web/pages/[workspaceSlug]/profile/[userId]/assigned.tsx b/web/pages/[workspaceSlug]/profile/[userId]/assigned.tsx index a0b6e9d6f..6d0c6e0d6 100644 --- a/web/pages/[workspaceSlug]/profile/[userId]/assigned.tsx +++ b/web/pages/[workspaceSlug]/profile/[userId]/assigned.tsx @@ -9,6 +9,7 @@ import { ProfileAuthWrapper } from "layouts/profile-layout"; import { UserProfileHeader } from "components/headers"; import { ProfileIssuesListLayout } from "components/issues/issue-layouts/list/roots/profile-issues-root"; import { ProfileIssuesKanBanLayout } from "components/issues/issue-layouts/kanban/roots/profile-issues-root"; +import { Spinner } from "@plane/ui"; // hooks import { useMobxStore } from "lib/mobx/store-provider"; import { RootStore } from "store/root"; @@ -46,7 +47,9 @@ const ProfileAssignedIssuesPage: NextPageWithLayout = observer(() => { return ( <> {isLoading ? ( -
Loading...
+
+ +
) : (
{activeLayout === "list" ? ( diff --git a/web/pages/[workspaceSlug]/profile/[userId]/created.tsx b/web/pages/[workspaceSlug]/profile/[userId]/created.tsx index 152e06218..7bf21edd9 100644 --- a/web/pages/[workspaceSlug]/profile/[userId]/created.tsx +++ b/web/pages/[workspaceSlug]/profile/[userId]/created.tsx @@ -11,6 +11,7 @@ import { ProfileAuthWrapper } from "layouts/profile-layout"; import { UserProfileHeader } from "components/headers"; import { ProfileIssuesListLayout } from "components/issues/issue-layouts/list/roots/profile-issues-root"; import { ProfileIssuesKanBanLayout } from "components/issues/issue-layouts/kanban/roots/profile-issues-root"; +import { Spinner } from "@plane/ui"; // types import { NextPageWithLayout } from "types/app"; @@ -42,7 +43,9 @@ const ProfileCreatedIssuesPage: NextPageWithLayout = () => { return ( <> {isLoading ? ( -
Loading...
+
+ +
) : (
{activeLayout === "list" ? ( diff --git a/web/pages/[workspaceSlug]/profile/[userId]/subscribed.tsx b/web/pages/[workspaceSlug]/profile/[userId]/subscribed.tsx index a37d9dbde..8900fb3fd 100644 --- a/web/pages/[workspaceSlug]/profile/[userId]/subscribed.tsx +++ b/web/pages/[workspaceSlug]/profile/[userId]/subscribed.tsx @@ -11,6 +11,7 @@ import { ProfileAuthWrapper } from "layouts/profile-layout"; import { UserProfileHeader } from "components/headers"; import { ProfileIssuesListLayout } from "components/issues/issue-layouts/list/roots/profile-issues-root"; import { ProfileIssuesKanBanLayout } from "components/issues/issue-layouts/kanban/roots/profile-issues-root"; +import { Spinner } from "@plane/ui"; // types import { NextPageWithLayout } from "types/app"; @@ -40,21 +41,21 @@ const ProfileSubscribedIssuesPage: NextPageWithLayout = () => { const activeLayout = profileIssueFiltersStore.userDisplayFilters.layout; return ( - }> - - {isLoading ? ( -
Loading...
- ) : ( -
- {activeLayout === "list" ? ( - - ) : activeLayout === "kanban" ? ( - - ) : null} -
- )} -
-
+ <> + {isLoading ? ( +
+ +
+ ) : ( +
+ {activeLayout === "list" ? ( + + ) : activeLayout === "kanban" ? ( + + ) : null} +
+ )} + ); }; diff --git a/web/store/issue/issue_filters.store.ts b/web/store/issue/issue_filters.store.ts index 91f86b713..f6e84c622 100644 --- a/web/store/issue/issue_filters.store.ts +++ b/web/store/issue/issue_filters.store.ts @@ -186,6 +186,13 @@ export class IssueFilterStore implements IIssueFilterStore { // set sub_group_by to null if group_by is set to null if (newViewProps.display_filters.group_by === null) newViewProps.display_filters.sub_group_by = null; + // set sub_group_by to null if layout is switched to kanban group_by and sub_group_by are same + if ( + newViewProps.display_filters.layout === "kanban" && + newViewProps.display_filters.group_by === newViewProps.display_filters.sub_group_by + ) + newViewProps.display_filters.sub_group_by = null; + // set group_by to state if layout is switched to kanban and group_by is null if (newViewProps.display_filters.layout === "kanban" && newViewProps.display_filters.group_by === null) newViewProps.display_filters.group_by = "state";