fix: issue property dropdown data flow (#3425)

* dev: workspace states and estimates

* refactor issue dropdown logic to help work properly with issues on global level

* fix: project labels response change

* fix label type

* change store computed actions to computed functions from mobx-utils

* fix: state response change

* chore: project and workspace state change

* fix state and label types

* chore: state and label serializer change

* modify state and label types

* fix dropdown reset on project id change

* fix label sort order

---------

Co-authored-by: pablohashescobar <nikhilschacko@gmail.com>
Co-authored-by: Rahul R <rahulr@Rahuls-MacBook-Pro.local>
Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
Co-authored-by: Rahul R <rahul.ramesha@plane.so>
This commit is contained in:
rahulramesha 2024-01-22 17:07:32 +05:30 committed by GitHub
parent be62662bb1
commit b3ac9def8d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
82 changed files with 494 additions and 463 deletions

View file

@ -72,9 +72,7 @@ const UserLink = ({ activity }: { activity: IIssueActivity }) => {
const LabelPill = observer(({ labelId, workspaceSlug }: { labelId: string; workspaceSlug: string }) => {
// store hooks
const {
workspace: { workspaceLabels, fetchWorkspaceLabels },
} = useLabel();
const { workspaceLabels, fetchWorkspaceLabels } = useLabel();
useEffect(() => {
if (!workspaceLabels) fetchWorkspaceLabels(workspaceSlug);

View file

@ -158,17 +158,12 @@ export const EstimateDropdown: React.FC<Props> = observer((props) => {
const filteredOptions =
query === "" ? options : options?.filter((o) => o.query.toLowerCase().includes(query.toLowerCase()));
// fetch cycles of the project if not already present in the store
useEffect(() => {
if (!workspaceSlug) return;
if (!activeEstimate) fetchProjectEstimates(workspaceSlug, projectId);
}, [activeEstimate, fetchProjectEstimates, projectId, workspaceSlug]);
const selectedEstimate = value !== null ? getEstimatePointValue(value) : null;
const selectedEstimate = value !== null ? getEstimatePointValue(value, projectId) : null;
const openDropdown = () => {
setIsOpen(true);
if (!activeEstimate && workspaceSlug) fetchProjectEstimates(workspaceSlug, projectId);
if (referenceElement) referenceElement.focus();
};
const closeDropdown = () => setIsOpen(false);

View file

@ -93,14 +93,10 @@ export const ProjectMemberDropdown: React.FC<Props> = observer((props) => {
};
if (multiple) comboboxProps.multiple = true;
useEffect(() => {
if (!workspaceSlug) return;
if (!projectMemberIds) fetchProjectMembers(workspaceSlug, projectId);
}, [fetchProjectMembers, projectId, projectMemberIds, workspaceSlug]);
const openDropdown = () => {
setIsOpen(true);
if (!projectMemberIds && workspaceSlug) fetchProjectMembers(workspaceSlug, projectId);
if (referenceElement) referenceElement.focus();
};
const closeDropdown = () => setIsOpen(false);

View file

@ -142,17 +142,12 @@ export const StateDropdown: React.FC<Props> = observer((props) => {
const filteredOptions =
query === "" ? options : options?.filter((o) => o.query.toLowerCase().includes(query.toLowerCase()));
// fetch states of the project if not already present in the store
useEffect(() => {
if (!workspaceSlug) return;
if (!statesList) fetchProjectStates(workspaceSlug, projectId);
}, [fetchProjectStates, projectId, statesList, workspaceSlug]);
const selectedState = getStateById(value);
const openDropdown = () => {
setIsOpen(true);
if (!statesList && workspaceSlug) fetchProjectStates(workspaceSlug, projectId);
if (referenceElement) referenceElement.focus();
};
const closeDropdown = () => setIsOpen(false);

View file

@ -75,9 +75,7 @@ export const CycleIssuesHeader: React.FC = observer(() => {
} = useUser();
const { currentProjectDetails } = useProject();
const { projectStates } = useProjectState();
const {
project: { projectLabels },
} = useLabel();
const { projectLabels } = useLabel();
const {
project: { projectMemberIds },
} = useMember();

View file

@ -45,9 +45,7 @@ export const GlobalIssuesHeader: React.FC<Props> = observer((props) => {
const {
membership: { currentWorkspaceRole },
} = useUser();
const {
workspace: { workspaceLabels },
} = useLabel();
const { workspaceLabels } = useLabel();
const {
workspace: { workspaceMemberIds },
} = useMember();

View file

@ -77,9 +77,7 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
membership: { currentProjectRole },
} = useUser();
const { currentProjectDetails } = useProject();
const {
project: { projectLabels },
} = useLabel();
const { projectLabels } = useLabel();
const { projectStates } = useProjectState();
const {
project: { projectMemberIds },

View file

@ -25,9 +25,7 @@ export const ProjectArchivedIssuesHeader: FC = observer(() => {
} = useIssues(EIssuesStoreType.ARCHIVED);
const { currentProjectDetails } = useProject();
const { projectStates } = useProjectState();
const {
project: { projectLabels },
} = useLabel();
const { projectLabels } = useLabel();
const {
project: { projectMemberIds },
} = useMember();

View file

@ -22,9 +22,7 @@ export const ProjectDraftIssueHeader: FC = observer(() => {
} = useIssues(EIssuesStoreType.DRAFT);
const { currentProjectDetails } = useProject();
const { projectStates } = useProjectState();
const {
project: { projectLabels },
} = useLabel();
const { projectLabels } = useLabel();
const {
project: { projectMemberIds },
} = useMember();

View file

@ -42,9 +42,7 @@ export const ProjectIssuesHeader: React.FC = observer(() => {
} = useUser();
const { currentProjectDetails } = useProject();
const { projectStates } = useProjectState();
const {
project: { projectLabels },
} = useLabel();
const { projectLabels } = useLabel();
const activeLayout = issueFilters?.displayFilters?.layout;

View file

@ -49,9 +49,7 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
const { currentProjectDetails } = useProject();
const { projectViewIds, getViewById } = useProjectView();
const { projectStates } = useProjectState();
const {
project: { projectLabels },
} = useLabel();
const { projectLabels } = useLabel();
const {
project: { projectMemberIds },
} = useMember();

View file

@ -24,9 +24,7 @@ export const IssueLabel: FC<TIssueLabel> = observer((props) => {
const { workspaceSlug, projectId, issueId, disabled = false } = props;
// hooks
const { updateIssue } = useIssueDetail();
const {
project: { createLabel },
} = useLabel();
const { createLabel } = useLabel();
const { setToastAlert } = useToast();
const labelOperations: TLabelOperations = useMemo(

View file

@ -20,9 +20,7 @@ export const IssueLabelSelect: React.FC<IIssueLabelSelect> = observer((props) =>
const {
issue: { getIssueById },
} = useIssueDetail();
const {
project: { fetchProjectLabels, projectLabels },
} = useLabel();
const { fetchProjectLabels, getProjectLabels } = useLabel();
// states
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
@ -30,10 +28,12 @@ export const IssueLabelSelect: React.FC<IIssueLabelSelect> = observer((props) =>
const [query, setQuery] = useState("");
const issue = getIssueById(issueId);
const projectLabels = getProjectLabels(projectId);
const fetchLabels = () => {
setIsLoading(true);
if (workspaceSlug && projectId) fetchProjectLabels(workspaceSlug, projectId).then(() => setIsLoading(false));
if (!projectLabels && workspaceSlug && projectId)
fetchProjectLabels(workspaceSlug, projectId).then(() => setIsLoading(false));
};
const options = (projectLabels ?? []).map((label) => ({

View file

@ -17,9 +17,7 @@ export const ArchivedIssueAppliedFiltersRoot: React.FC = observer(() => {
const {
issuesFilter: { issueFilters, updateFilters },
} = useIssues(EIssuesStoreType.ARCHIVED);
const {
project: { projectLabels },
} = useLabel();
const { projectLabels } = useLabel();
const { projectStates } = useProjectState();
// derived values
const userFilters = issueFilters?.filters;

View file

@ -21,9 +21,7 @@ export const CycleAppliedFiltersRoot: React.FC = observer(() => {
issuesFilter: { issueFilters, updateFilters },
} = useIssues(EIssuesStoreType.CYCLE);
const {
project: { projectLabels },
} = useLabel();
const { projectLabels } = useLabel();
const { projectStates } = useProjectState();
// derived values
const userFilters = issueFilters?.filters;

View file

@ -16,9 +16,7 @@ export const DraftIssueAppliedFiltersRoot: React.FC = observer(() => {
const {
issuesFilter: { issueFilters, updateFilters },
} = useIssues(EIssuesStoreType.DRAFT);
const {
project: { projectLabels },
} = useLabel();
const { projectLabels } = useLabel();
const { projectStates } = useProjectState();
// derived values
const userFilters = issueFilters?.filters;

View file

@ -25,9 +25,7 @@ export const GlobalViewsAppliedFiltersRoot = observer((props: Props) => {
const {
issuesFilter: { filters, updateFilters },
} = useIssues(EIssuesStoreType.GLOBAL);
const {
workspace: { workspaceLabels },
} = useLabel();
const { workspaceLabels } = useLabel();
const { globalViewMap, updateGlobalView } = useGlobalView();
const {
membership: { currentWorkspaceRole },

View file

@ -20,9 +20,7 @@ export const ModuleAppliedFiltersRoot: React.FC = observer(() => {
const {
issuesFilter: { issueFilters, updateFilters },
} = useIssues(EIssuesStoreType.MODULE);
const {
project: { projectLabels },
} = useLabel();
const { projectLabels } = useLabel();
const { projectStates } = useProjectState();
// derived values
const userFilters = issueFilters?.filters;

View file

@ -7,19 +7,20 @@ import { AppliedFiltersList } from "components/issues";
// types
import { IIssueFilterOptions } from "@plane/types";
import { EIssueFilterType, EIssuesStoreType } from "constants/issue";
import { useWorskspaceIssueProperties } from "hooks/use-worskspace-issue-properties";
export const ProfileIssuesAppliedFiltersRoot: React.FC = observer(() => {
// router
const router = useRouter();
const { workspaceSlug, userId } = router.query;
//swr hook for fetching issue properties
useWorskspaceIssueProperties(workspaceSlug);
// store hooks
const {
issuesFilter: { issueFilters, updateFilters },
} = useIssues(EIssuesStoreType.PROFILE);
const {
workspace: { workspaceLabels },
} = useLabel();
const { workspaceLabels } = useLabel();
// derived values
const userFilters = issueFilters?.filters;

View file

@ -19,9 +19,7 @@ export const ProjectAppliedFiltersRoot: React.FC = observer(() => {
projectId: string;
};
// store hooks
const {
project: { projectLabels },
} = useLabel();
const { projectLabels } = useLabel();
const {
issuesFilter: { issueFilters, updateFilters },
} = useIssues(EIssuesStoreType.PROJECT);

View file

@ -23,9 +23,7 @@ export const ProjectViewAppliedFiltersRoot: React.FC = observer(() => {
const {
issuesFilter: { issueFilters, updateFilters },
} = useIssues(EIssuesStoreType.PROJECT_VIEW);
const {
project: { projectLabels },
} = useLabel();
const { projectLabels } = useLabel();
const { projectStates } = useProjectState();
const { viewMap, updateView } = useProjectView();
// derived values

View file

@ -71,10 +71,10 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
const member = useMember();
const project = useProject();
const projectLabel = useLabel();
const label = useLabel();
const projectState = useProjectState();
const list = getGroupByColumns(group_by as GroupByColumnTypes, project, projectLabel, projectState, member);
const list = getGroupByColumns(group_by as GroupByColumnTypes, project, label, projectState, member);
if (!list) return null;

View file

@ -208,17 +208,11 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
const member = useMember();
const project = useProject();
const projectLabel = useLabel();
const label = useLabel();
const projectState = useProjectState();
const groupByList = getGroupByColumns(group_by as GroupByColumnTypes, project, projectLabel, projectState, member);
const subGroupByList = getGroupByColumns(
sub_group_by as GroupByColumnTypes,
project,
projectLabel,
projectState,
member
);
const groupByList = getGroupByColumns(group_by as GroupByColumnTypes, project, label, projectState, member);
const subGroupByList = getGroupByColumns(sub_group_by as GroupByColumnTypes, project, label, projectState, member);
if (!groupByList || !subGroupByList) return null;

View file

@ -59,10 +59,10 @@ const GroupByList: React.FC<IGroupByList> = (props) => {
// store hooks
const member = useMember();
const project = useProject();
const projectLabel = useLabel();
const label = useLabel();
const projectState = useProjectState();
const list = getGroupByColumns(group_by as GroupByColumnTypes, project, projectLabel, projectState, member, true);
const list = getGroupByColumns(group_by as GroupByColumnTypes, project, label, projectState, member, true);
if (!list) return null;

View file

@ -53,13 +53,15 @@ export const IssuePropertyLabels: React.FC<IIssuePropertyLabels> = observer((pro
const {
router: { workspaceSlug },
} = useApplication();
const {
project: { fetchProjectLabels, projectLabels: storeLabels },
} = useLabel();
const { fetchProjectLabels, getProjectLabels } = useLabel();
const fetchLabels = () => {
setIsLoading(true);
if (workspaceSlug && projectId) fetchProjectLabels(workspaceSlug, projectId).then(() => setIsLoading(false));
const storeLabels = getProjectLabels(projectId);
const openDropDown = () => {
if (!storeLabels && workspaceSlug && projectId) {
setIsLoading(true);
fetchProjectLabels(workspaceSlug, projectId).then(() => setIsLoading(false));
}
};
const { styles, attributes } = usePopper(referenceElement, popperElement, {
@ -182,7 +184,7 @@ export const IssuePropertyLabels: React.FC<IIssuePropertyLabels> = observer((pro
? "cursor-pointer"
: "cursor-pointer hover:bg-custom-background-80"
} ${buttonClassName}`}
onClick={() => !storeLabels && fetchLabels()}
onClick={openDropDown}
>
{label}
{!hideDropdownArrow && !disabled && <ChevronDown className="h-3 w-3" aria-hidden="true" />}

View file

@ -4,6 +4,7 @@ import { observer } from "mobx-react-lite";
import useSWR from "swr";
// hooks
import { useGlobalView, useIssues, useUser } from "hooks/store";
import { useWorskspaceIssueProperties } from "hooks/use-worskspace-issue-properties";
// components
import { GlobalViewsAppliedFiltersRoot, IssuePeekOverview } from "components/issues";
import { SpreadsheetView } from "components/issues/issue-layouts";
@ -20,7 +21,8 @@ export const AllIssueLayoutRoot: React.FC = observer(() => {
// router
const router = useRouter();
const { workspaceSlug, globalViewId } = router.query;
//swr hook for fetching issue properties
useWorskspaceIssueProperties(workspaceSlug);
// store
const {
issuesFilter: { filters, fetchFilters, updateFilters },

View file

@ -1,17 +1,17 @@
import { Avatar, PriorityIcon, StateGroupIcon } from "@plane/ui";
import { ISSUE_PRIORITIES } from "constants/issue";
import { renderEmoji } from "helpers/emoji.helper";
import { ILabelRootStore } from "store/label";
import { IMemberRootStore } from "store/member";
import { IProjectStore } from "store/project/project.store";
import { IStateStore } from "store/state.store";
import { GroupByColumnTypes, IGroupByColumn } from "@plane/types";
import { STATE_GROUPS } from "constants/state";
import { ILabelStore } from "store/label.store";
export const getGroupByColumns = (
groupBy: GroupByColumnTypes | null,
project: IProjectStore,
projectLabel: ILabelRootStore,
label: ILabelStore,
projectState: IStateStore,
member: IMemberRootStore,
includeNone?: boolean
@ -26,7 +26,7 @@ export const getGroupByColumns = (
case "priority":
return getPriorityColumns();
case "labels":
return getLabelsColumns(projectLabel) as any;
return getLabelsColumns(label) as any;
case "assignees":
return getAssigneeColumns(member) as any;
case "created_by":
@ -97,10 +97,8 @@ const getPriorityColumns = () => {
}));
};
const getLabelsColumns = (projectLabel: ILabelRootStore) => {
const {
project: { projectLabels },
} = projectLabel;
const getLabelsColumns = (label: ILabelStore) => {
const { projectLabels } = label;
if (!projectLabels) return;

View file

@ -64,7 +64,15 @@ const aiService = new AIService();
const fileService = new FileService();
export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
const { data, onChange, onClose, onSubmit, projectId, isCreateMoreToggleEnabled, onCreateMoreToggleChange } = props;
const {
data,
onChange,
onClose,
onSubmit,
projectId: defaultProjectId,
isCreateMoreToggleEnabled,
onCreateMoreToggleChange,
} = props;
// states
const [labelModal, setLabelModal] = useState(false);
const [parentIssueListModalOpen, setParentIssueListModalOpen] = useState(false);
@ -99,10 +107,30 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
getValues,
setValue,
} = useForm<TIssue>({
defaultValues: { ...defaultValues, project_id: projectId, ...data },
defaultValues: { ...defaultValues, project_id: defaultProjectId, ...data },
reValidateMode: "onChange",
});
const projectId = watch("project_id");
//reset few fields on projectId change
useEffect(() => {
if (isDirty) {
const formData = getValues();
reset({
...defaultValues,
project_id: projectId,
name: formData.name,
description_html: formData.description_html,
priority: formData.priority,
start_date: formData.start_date,
target_date: formData.target_date,
parent_id: formData.parent_id,
});
}
}, [projectId]);
const issueName = watch("name");
const handleFormSubmit = async (formData: Partial<TIssue>) => {
@ -130,7 +158,7 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
setIAmFeelingLucky(true);
aiService
.createGptTask(workspaceSlug.toString(), projectId.toString(), {
.createGptTask(workspaceSlug.toString(), projectId, {
prompt: issueName,
task: "Generate a proper description for this issue.",
})

View file

@ -23,14 +23,7 @@ export interface IssuesModalProps {
}
export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((props) => {
const {
data,
isOpen,
onClose,
onSubmit,
withDraftIssueWrapper = true,
storeType = EIssuesStoreType.PROJECT,
} = props;
const { data, isOpen, onClose, onSubmit, withDraftIssueWrapper = true, storeType = EIssuesStoreType.PROJECT } = props;
// states
const [changesMade, setChangesMade] = useState<Partial<TIssue> | null>(null);
const [createMore, setCreateMore] = useState(false);

View file

@ -29,9 +29,7 @@ export const IssueLabelSelect: React.FC<Props> = observer((props) => {
const router = useRouter();
const { workspaceSlug } = router.query;
// store hooks
const {
project: { getProjectLabels, fetchProjectLabels },
} = useLabel();
const { getProjectLabels, fetchProjectLabels } = useLabel();
// states
const [query, setQuery] = useState("");
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
@ -50,13 +48,9 @@ export const IssueLabelSelect: React.FC<Props> = observer((props) => {
const filteredOptions =
query === "" ? projectLabels : projectLabels?.filter((l) => l.name.toLowerCase().includes(query.toLowerCase()));
useSWR(
workspaceSlug && projectId ? `PROJECT_ISSUE_LABELS_${projectId.toUpperCase()}` : null,
workspaceSlug && projectId ? () => fetchProjectLabels(workspaceSlug.toString(), projectId) : null
);
const openDropdown = () => {
setIsDropdownOpen(true);
if (!projectLabels && workspaceSlug && projectId) fetchProjectLabels(workspaceSlug.toString(), projectId);
if (referenceElement) referenceElement.focus();
};
const closeDropdown = () => setIsDropdownOpen(false);

View file

@ -21,7 +21,7 @@ export const ViewEstimateSelect: React.FC<Props> = observer((props) => {
const { issue, onChange, tooltipPosition = "top", customButton = false, disabled } = props;
const { areEstimatesEnabledForCurrentProject, activeEstimateDetails, getEstimatePointValue } = useEstimate();
const estimateValue = getEstimatePointValue(issue.estimate_point);
const estimateValue = getEstimatePointValue(issue.estimate_point, issue.project_id);
const estimateLabels = (
<Tooltip tooltipHeading="Estimate" tooltipContent={estimateValue} position={tooltipPosition}>

View file

@ -34,9 +34,7 @@ export const CreateLabelModal: React.FC<Props> = observer((props) => {
const router = useRouter();
const { workspaceSlug } = router.query;
// store hooks
const {
project: { createLabel },
} = useLabel();
const { createLabel } = useLabel();
// form info
const {
formState: { errors, isSubmitting },

View file

@ -34,9 +34,7 @@ export const CreateUpdateLabelInline = observer(
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
// store hooks
const {
project: { createLabel, updateLabel },
} = useLabel();
const { createLabel, updateLabel } = useLabel();
// toast alert
const { setToastAlert } = useToast();
// form info

View file

@ -25,9 +25,7 @@ export const DeleteLabelModal: React.FC<Props> = observer((props) => {
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
// store hooks
const {
project: { deleteLabel },
} = useLabel();
const { deleteLabel } = useLabel();
// states
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
// hooks

View file

@ -25,9 +25,7 @@ export const LabelsListModal: React.FC<Props> = observer((props) => {
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
// store hooks
const {
project: { projectLabels, fetchProjectLabels, updateLabel },
} = useLabel();
const { projectLabels, fetchProjectLabels, updateLabel } = useLabel();
// api call to fetch project details
useSWR(

View file

@ -28,9 +28,7 @@ export const ProjectSettingLabelItem: React.FC<Props> = (props) => {
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
// store hooks
const {
project: { updateLabel },
} = useLabel();
const { updateLabel } = useLabel();
const removeFromGroup = (label: IIssueLabel) => {
if (!workspaceSlug || !projectId) return;

View file

@ -41,9 +41,7 @@ export const ProjectSettingsLabelList: React.FC = observer(() => {
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
// store hooks
const {
project: { projectLabels, updateLabelPosition, projectLabelsTree },
} = useLabel();
const { projectLabels, updateLabelPosition, projectLabelsTree } = useLabel();
// portal
const renderDraggable = useDraggableInPortal();

View file

@ -18,9 +18,7 @@ export const ProfileIssuesFilter = observer(() => {
issuesFilter: { issueFilters, updateFilters },
} = useIssues(EIssuesStoreType.PROFILE);
const {
workspace: { workspaceLabels },
} = useLabel();
const { workspaceLabels } = useLabel();
// derived values
const states = undefined;
const members = undefined;

View file

@ -42,7 +42,7 @@ export const DeleteStateModal: React.FC<Props> = observer((props) => {
setIsDeleteLoading(true);
await deleteState(workspaceSlug.toString(), data.project, data.id)
await deleteState(workspaceSlug.toString(), data.project_id, data.id)
.then(() => {
postHogEventTracker("STATE_DELETE", {
state: "SUCCESS",

View file

@ -28,9 +28,7 @@ export const ProjectViewForm: React.FC<Props> = observer((props) => {
const { handleFormSubmit, handleClose, data, preLoadedData } = props;
// store hooks
const { projectStates } = useProjectState();
const {
project: { projectLabels },
} = useLabel();
const { projectLabels } = useLabel();
const {
project: { projectMemberIds },
} = useMember();

View file

@ -27,9 +27,7 @@ const defaultValues: Partial<IWorkspaceView> = {
export const WorkspaceViewForm: React.FC<Props> = observer((props) => {
const { handleFormSubmit, handleClose, data, preLoadedData } = props;
// store hooks
const {
workspace: { workspaceLabels },
} = useLabel();
const { workspaceLabels } = useLabel();
const {
workspace: { workspaceMemberIds },
} = useMember();