[WEB-756] chore: module and cycle feature toggle validation (#4112)

* chore: cycle and module feature issue block validation

* chore: cycle and module feature display properties validation

* chore: cycle and module feature display filters validation

* chore: cycle and module feature project view validation
This commit is contained in:
Anmol Singh Bhatia 2024-04-03 20:49:02 +05:30 committed by GitHub
parent bc0752f7e8
commit e9518ced89
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 144 additions and 59 deletions

View File

@ -10,7 +10,7 @@ import { CustomMenu } from "@plane/ui";
import { ProjectAnalyticsModal } from "@/components/analytics";
import { DisplayFiltersSelection, FilterSelection, FiltersDropdown } from "@/components/issues";
import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT, ISSUE_LAYOUTS } from "@/constants/issue";
import { useIssues, useCycle, useProjectState, useLabel, useMember } from "@/hooks/store";
import { useIssues, useCycle, useProjectState, useLabel, useMember, useProject } from "@/hooks/store";
export const CycleMobileHeader = () => {
const [analyticsModal, setAnalyticsModal] = useState(false);
@ -24,6 +24,7 @@ export const CycleMobileHeader = () => {
const { workspaceSlug, projectId, cycleId } = router.query;
const cycleDetails = cycleId ? getCycleById(cycleId.toString()) : undefined;
// store hooks
const { currentProjectDetails } = useProject();
const {
issuesFilter: { issueFilters, updateFilters },
} = useIssues(EIssuesStoreType.CYCLE);
@ -151,6 +152,8 @@ export const CycleMobileHeader = () => {
labels={projectLabels}
memberIds={projectMemberIds ?? undefined}
states={projectStates}
cycleViewDisabled={!currentProjectDetails?.cycle_view}
moduleViewDisabled={!currentProjectDetails?.module_view}
/>
</FiltersDropdown>
</div>
@ -174,6 +177,8 @@ export const CycleMobileHeader = () => {
displayProperties={issueFilters?.displayProperties ?? {}}
handleDisplayPropertiesUpdate={handleDisplayProperties}
ignoreGroupedFilters={["cycle"]}
cycleViewDisabled={!currentProjectDetails?.cycle_view}
moduleViewDisabled={!currentProjectDetails?.module_view}
/>
</FiltersDropdown>
</div>

View File

@ -250,6 +250,8 @@ export const CycleIssuesHeader: React.FC = observer(() => {
labels={projectLabels}
memberIds={projectMemberIds ?? undefined}
states={projectStates}
cycleViewDisabled={!currentProjectDetails?.cycle_view}
moduleViewDisabled={!currentProjectDetails?.module_view}
/>
</FiltersDropdown>
<FiltersDropdown title="Display" placement="bottom-end">
@ -262,6 +264,8 @@ export const CycleIssuesHeader: React.FC = observer(() => {
displayProperties={issueFilters?.displayProperties ?? {}}
handleDisplayPropertiesUpdate={handleDisplayProperties}
ignoreGroupedFilters={["cycle"]}
cycleViewDisabled={!currentProjectDetails?.cycle_view}
moduleViewDisabled={!currentProjectDetails?.module_view}
/>
</FiltersDropdown>

View File

@ -252,6 +252,8 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
labels={projectLabels}
memberIds={projectMemberIds ?? undefined}
states={projectStates}
cycleViewDisabled={!currentProjectDetails?.cycle_view}
moduleViewDisabled={!currentProjectDetails?.module_view}
/>
</FiltersDropdown>
<FiltersDropdown title="Display" placement="bottom-end">
@ -264,6 +266,8 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
displayProperties={issueFilters?.displayProperties ?? {}}
handleDisplayPropertiesUpdate={handleDisplayProperties}
ignoreGroupedFilters={["module"]}
cycleViewDisabled={!currentProjectDetails?.cycle_view}
moduleViewDisabled={!currentProjectDetails?.module_view}
/>
</FiltersDropdown>
</div>

View File

@ -141,6 +141,8 @@ export const ProjectDraftIssueHeader: FC = observer(() => {
labels={projectLabels}
memberIds={projectMemberIds ?? undefined}
states={projectStates}
cycleViewDisabled={!currentProjectDetails?.cycle_view}
moduleViewDisabled={!currentProjectDetails?.module_view}
/>
</FiltersDropdown>
<FiltersDropdown title="Display" placement="bottom-end">
@ -152,6 +154,8 @@ export const ProjectDraftIssueHeader: FC = observer(() => {
handleDisplayFiltersUpdate={handleDisplayFilters}
displayProperties={issueFilters?.displayProperties ?? {}}
handleDisplayPropertiesUpdate={handleDisplayProperties}
cycleViewDisabled={!currentProjectDetails?.cycle_view}
moduleViewDisabled={!currentProjectDetails?.module_view}
/>
</FiltersDropdown>
</div>

View File

@ -192,6 +192,8 @@ export const ProjectIssuesHeader: React.FC = observer(() => {
labels={projectLabels}
memberIds={projectMemberIds ?? undefined}
states={projectStates}
cycleViewDisabled={!currentProjectDetails?.cycle_view}
moduleViewDisabled={!currentProjectDetails?.module_view}
/>
</FiltersDropdown>
<FiltersDropdown title="Display" placement="bottom-end">
@ -203,6 +205,8 @@ export const ProjectIssuesHeader: React.FC = observer(() => {
handleDisplayFiltersUpdate={handleDisplayFilters}
displayProperties={issueFilters?.displayProperties ?? {}}
handleDisplayPropertiesUpdate={handleDisplayProperties}
cycleViewDisabled={!currentProjectDetails?.cycle_view}
moduleViewDisabled={!currentProjectDetails?.module_view}
/>
</FiltersDropdown>
</div>

View File

@ -210,6 +210,8 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
labels={projectLabels}
memberIds={projectMemberIds ?? undefined}
states={projectStates}
cycleViewDisabled={!currentProjectDetails?.cycle_view}
moduleViewDisabled={!currentProjectDetails?.module_view}
/>
</FiltersDropdown>
<FiltersDropdown title="Display" placement="bottom-end">
@ -221,6 +223,8 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
handleDisplayFiltersUpdate={handleDisplayFilters}
displayProperties={issueFilters?.displayProperties ?? {}}
handleDisplayPropertiesUpdate={handleDisplayProperties}
cycleViewDisabled={!currentProjectDetails?.cycle_view}
moduleViewDisabled={!currentProjectDetails?.module_view}
/>
</FiltersDropdown>
{canUserCreateIssue && (

View File

@ -9,13 +9,14 @@ import { DisplayFiltersSelection, FilterSelection, FiltersDropdown } from "@/com
// constants
import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/constants/issue";
// hooks
import { useIssues, useLabel, useMember, useProjectState } from "@/hooks/store";
import { useIssues, useLabel, useMember, useProject, useProjectState } from "@/hooks/store";
export const ArchivedIssuesHeader: FC = observer(() => {
// router
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
// store hooks
const { currentProjectDetails } = useProject();
const {
issuesFilter: { issueFilters, updateFilters },
} = useIssues(EIssuesStoreType.ARCHIVED);
@ -89,6 +90,8 @@ export const ArchivedIssuesHeader: FC = observer(() => {
layoutDisplayFiltersOptions={
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
}
cycleViewDisabled={!currentProjectDetails?.cycle_view}
moduleViewDisabled={!currentProjectDetails?.module_view}
/>
</FiltersDropdown>
</div>

View File

@ -21,6 +21,8 @@ type Props = {
handleDisplayPropertiesUpdate: (updatedDisplayProperties: Partial<IIssueDisplayProperties>) => void;
layoutDisplayFiltersOptions: ILayoutDisplayFiltersOptions | undefined;
ignoreGroupedFilters?: Partial<TIssueGroupByOptions>[];
cycleViewDisabled?: boolean;
moduleViewDisabled?: boolean;
};
export const DisplayFiltersSelection: React.FC<Props> = observer((props) => {
@ -31,17 +33,32 @@ export const DisplayFiltersSelection: React.FC<Props> = observer((props) => {
handleDisplayPropertiesUpdate,
layoutDisplayFiltersOptions,
ignoreGroupedFilters = [],
cycleViewDisabled = false,
moduleViewDisabled = false,
} = props;
const isDisplayFilterEnabled = (displayFilter: keyof IIssueDisplayFilterOptions) =>
Object.keys(layoutDisplayFiltersOptions?.display_filters ?? {}).includes(displayFilter);
const computedIgnoreGroupedFilters: Partial<TIssueGroupByOptions>[] = [];
if (cycleViewDisabled) {
ignoreGroupedFilters.push("cycle");
}
if (moduleViewDisabled) {
ignoreGroupedFilters.push("module");
}
return (
<div className="vertical-scrollbar scrollbar-sm relative h-full w-full divide-y divide-custom-border-200 overflow-hidden overflow-y-auto px-2.5">
{/* display properties */}
{layoutDisplayFiltersOptions?.display_properties && (
<div className="py-2">
<FilterDisplayProperties displayProperties={displayProperties} handleUpdate={handleDisplayPropertiesUpdate} />
<FilterDisplayProperties
displayProperties={displayProperties}
handleUpdate={handleDisplayPropertiesUpdate}
cycleViewDisabled={cycleViewDisabled}
moduleViewDisabled={moduleViewDisabled}
/>
</div>
)}
@ -56,7 +73,7 @@ export const DisplayFiltersSelection: React.FC<Props> = observer((props) => {
group_by: val,
})
}
ignoreGroupedFilters={ignoreGroupedFilters}
ignoreGroupedFilters={[...ignoreGroupedFilters, ...computedIgnoreGroupedFilters]}
/>
</div>
)}
@ -74,7 +91,7 @@ export const DisplayFiltersSelection: React.FC<Props> = observer((props) => {
})
}
subGroupByOptions={layoutDisplayFiltersOptions?.display_filters.sub_group_by ?? []}
ignoreGroupedFilters={ignoreGroupedFilters}
ignoreGroupedFilters={[...ignoreGroupedFilters, ...computedIgnoreGroupedFilters]}
/>
</div>
)}

View File

@ -10,13 +10,22 @@ import { FilterHeader } from "../helpers/filter-header";
type Props = {
displayProperties: IIssueDisplayProperties;
handleUpdate: (updatedDisplayProperties: Partial<IIssueDisplayProperties>) => void;
cycleViewDisabled?: boolean;
moduleViewDisabled?: boolean;
};
export const FilterDisplayProperties: React.FC<Props> = observer((props) => {
const { displayProperties, handleUpdate } = props;
const { displayProperties, handleUpdate, cycleViewDisabled = false, moduleViewDisabled = false } = props;
const [previewEnabled, setPreviewEnabled] = React.useState(true);
// Filter out "cycle" and "module" keys if cycleViewDisabled or moduleViewDisabled is true
const filteredDisplayProperties = ISSUE_DISPLAY_PROPERTIES.filter((property) => {
if (cycleViewDisabled && property.key === "cycle") return false;
if (moduleViewDisabled && property.key === "modules") return false;
return true;
});
return (
<>
<FilterHeader
@ -26,23 +35,25 @@ export const FilterDisplayProperties: React.FC<Props> = observer((props) => {
/>
{previewEnabled && (
<div className="mt-1 flex flex-wrap items-center gap-2">
{ISSUE_DISPLAY_PROPERTIES.map((displayProperty) => (
<button
key={displayProperty.key}
type="button"
className={`rounded border px-2 py-0.5 text-xs transition-all ${
displayProperties?.[displayProperty.key]
? "border-custom-primary-100 bg-custom-primary-100 text-white"
: "border-custom-border-200 hover:bg-custom-background-80"
}`}
onClick={() =>
handleUpdate({
[displayProperty.key]: !displayProperties?.[displayProperty.key],
})
}
>
{displayProperty.title}
</button>
{filteredDisplayProperties.map((displayProperty) => (
<>
<button
key={displayProperty.key}
type="button"
className={`rounded border px-2 py-0.5 text-xs transition-all ${
displayProperties?.[displayProperty.key]
? "border-custom-primary-100 bg-custom-primary-100 text-white"
: "border-custom-border-200 hover:bg-custom-background-80"
}`}
onClick={() =>
handleUpdate({
[displayProperty.key]: !displayProperties?.[displayProperty.key],
})
}
>
{displayProperty.title}
</button>
</>
))}
</div>
)}

View File

@ -30,10 +30,21 @@ type Props = {
labels?: IIssueLabel[] | undefined;
memberIds?: string[] | undefined;
states?: IState[] | undefined;
cycleViewDisabled?: boolean;
moduleViewDisabled?: boolean;
};
export const FilterSelection: React.FC<Props> = observer((props) => {
const { filters, handleFiltersUpdate, layoutDisplayFiltersOptions, labels, memberIds, states } = props;
const {
filters,
handleFiltersUpdate,
layoutDisplayFiltersOptions,
labels,
memberIds,
states,
cycleViewDisabled = false,
moduleViewDisabled = false,
} = props;
// hooks
const {
router: { moduleId, cycleId },
@ -111,7 +122,7 @@ export const FilterSelection: React.FC<Props> = observer((props) => {
)}
{/* cycle */}
{isFilterEnabled("cycle") && !cycleId && (
{isFilterEnabled("cycle") && !cycleId && !cycleViewDisabled && (
<div className="py-2">
<FilterCycle
appliedFilters={filters.cycle ?? null}
@ -122,7 +133,7 @@ export const FilterSelection: React.FC<Props> = observer((props) => {
)}
{/* module */}
{isFilterEnabled("module") && !moduleId && (
{isFilterEnabled("module") && !moduleId && !moduleViewDisabled && (
<div className="py-2">
<FilterModule
appliedFilters={filters.module ?? null}

View File

@ -24,7 +24,7 @@ import { EIssuesStoreType } from "@/constants/issue";
import { cn } from "@/helpers/common.helper";
import { getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper";
import { shouldHighlightIssueDueDate } from "@/helpers/issue.helper";
import { useEventTracker, useEstimate, useLabel, useIssues, useProjectState } from "@/hooks/store";
import { useEventTracker, useEstimate, useLabel, useIssues, useProjectState, useProject } from "@/hooks/store";
import { usePlatformOS } from "@/hooks/use-platform-os";
// components
import { IssuePropertyLabels } from "../properties/labels";
@ -45,6 +45,7 @@ export interface IIssueProperties {
export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
const { issue, updateIssue, displayProperties, activeLayout, isReadOnly, className } = props;
// store hooks
const { getProjectById } = useProject();
const { labelMap } = useLabel();
const { captureIssueEvent } = useEventTracker();
const {
@ -56,6 +57,7 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
const { areEstimatesEnabledForCurrentProject } = useEstimate();
const { getStateById } = useProjectState();
const { isMobile } = usePlatformOS();
const projectDetails = getProjectById(issue.project_id);
// router
const router = useRouter();
const { workspaceSlug } = router.query;
@ -349,36 +351,40 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
</WithDisplayPropertiesHOC>
{/* modules */}
<WithDisplayPropertiesHOC displayProperties={displayProperties} displayPropertyKey="modules">
<div className="h-5">
<ModuleDropdown
buttonContainerClassName="truncate max-w-40"
projectId={issue?.project_id}
value={issue?.module_ids ?? []}
onChange={handleModule}
disabled={isReadOnly}
multiple
buttonVariant="border-with-text"
showCount
showTooltip
/>
</div>
</WithDisplayPropertiesHOC>
{projectDetails?.module_view && (
<WithDisplayPropertiesHOC displayProperties={displayProperties} displayPropertyKey="modules">
<div className="h-5">
<ModuleDropdown
buttonContainerClassName="truncate max-w-40"
projectId={issue?.project_id}
value={issue?.module_ids ?? []}
onChange={handleModule}
disabled={isReadOnly}
multiple
buttonVariant="border-with-text"
showCount
showTooltip
/>
</div>
</WithDisplayPropertiesHOC>
)}
{/* cycles */}
<WithDisplayPropertiesHOC displayProperties={displayProperties} displayPropertyKey="cycle">
<div className="h-5">
<CycleDropdown
buttonContainerClassName="truncate max-w-40"
projectId={issue?.project_id}
value={issue?.cycle_id}
onChange={handleCycle}
disabled={isReadOnly}
buttonVariant="border-with-text"
showTooltip
/>
</div>
</WithDisplayPropertiesHOC>
{projectDetails?.cycle_view && (
<WithDisplayPropertiesHOC displayProperties={displayProperties} displayPropertyKey="cycle">
<div className="h-5">
<CycleDropdown
buttonContainerClassName="truncate max-w-40"
projectId={issue?.project_id}
value={issue?.cycle_id}
onChange={handleCycle}
disabled={isReadOnly}
buttonVariant="border-with-text"
showTooltip
/>
</div>
</WithDisplayPropertiesHOC>
)}
{/* estimates */}
{areEstimatesEnabledForCurrentProject && (

View File

@ -132,6 +132,8 @@ export const IssuesMobileHeader = observer(() => {
labels={projectLabels}
memberIds={projectMemberIds ?? undefined}
states={projectStates}
cycleViewDisabled={!currentProjectDetails?.cycle_view}
moduleViewDisabled={!currentProjectDetails?.module_view}
/>
</FiltersDropdown>
</div>
@ -154,6 +156,8 @@ export const IssuesMobileHeader = observer(() => {
handleDisplayFiltersUpdate={handleDisplayFilters}
displayProperties={issueFilters?.displayProperties ?? {}}
handleDisplayPropertiesUpdate={handleDisplayProperties}
cycleViewDisabled={!currentProjectDetails?.cycle_view}
moduleViewDisabled={!currentProjectDetails?.module_view}
/>
</FiltersDropdown>
</div>

View File

@ -11,12 +11,13 @@ import { ProjectAnalyticsModal } from "@/components/analytics";
import { DisplayFiltersSelection, FilterSelection, FiltersDropdown } from "@/components/issues";
// hooks
import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT, ISSUE_LAYOUTS } from "@/constants/issue";
import { useIssues, useLabel, useMember, useModule, useProjectState } from "@/hooks/store";
import { useIssues, useLabel, useMember, useModule, useProject, useProjectState } from "@/hooks/store";
// types
// constants
export const ModuleMobileHeader = observer(() => {
const [analyticsModal, setAnalyticsModal] = useState(false);
const { currentProjectDetails } = useProject();
const { getModuleById } = useModule();
const layouts = [
{ key: "list", title: "List", icon: List },
@ -134,6 +135,8 @@ export const ModuleMobileHeader = observer(() => {
labels={projectLabels}
memberIds={projectMemberIds ?? undefined}
states={projectStates}
cycleViewDisabled={!currentProjectDetails?.cycle_view}
moduleViewDisabled={!currentProjectDetails?.module_view}
/>
</FiltersDropdown>
</div>
@ -157,6 +160,8 @@ export const ModuleMobileHeader = observer(() => {
displayProperties={issueFilters?.displayProperties ?? {}}
handleDisplayPropertiesUpdate={handleDisplayProperties}
ignoreGroupedFilters={["module"]}
cycleViewDisabled={!currentProjectDetails?.cycle_view}
moduleViewDisabled={!currentProjectDetails?.module_view}
/>
</FiltersDropdown>
</div>

View File

@ -6,7 +6,7 @@ import { IProjectView, IIssueFilterOptions } from "@plane/types";
import { Button, Input, TextArea } from "@plane/ui";
import { AppliedFiltersList, FilterSelection, FiltersDropdown } from "@/components/issues";
import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/constants/issue";
import { useLabel, useMember, useProjectState } from "@/hooks/store";
import { useLabel, useMember, useProject, useProjectState } from "@/hooks/store";
// components
// ui
// types
@ -27,6 +27,7 @@ const defaultValues: Partial<IProjectView> = {
export const ProjectViewForm: React.FC<Props> = observer((props) => {
const { handleFormSubmit, handleClose, data, preLoadedData } = props;
// store hooks
const { currentProjectDetails } = useProject();
const { projectStates } = useProjectState();
const { projectLabels } = useLabel();
const {
@ -184,6 +185,8 @@ export const ProjectViewForm: React.FC<Props> = observer((props) => {
labels={projectLabels ?? undefined}
memberIds={projectMemberIds ?? undefined}
states={projectStates}
cycleViewDisabled={!currentProjectDetails?.cycle_view}
moduleViewDisabled={!currentProjectDetails?.module_view}
/>
</FiltersDropdown>
)}
@ -212,8 +215,8 @@ export const ProjectViewForm: React.FC<Props> = observer((props) => {
? "Updating View..."
: "Update View"
: isSubmitting
? "Creating View..."
: "Create View"}
? "Creating View..."
: "Create View"}
</Button>
</div>
</form>