Skip to content

Commit 749a172

Browse files
enhance: Enhancement/more repository view filters (#49)
* fix: update date handling in forms and schemas - Changed `startedAt` and `completedAt` fields to be nullable in MilestoneFormDialog and AddMilestoneModal. - Updated date handling in AddCase, BulkEditModal, and various components to ensure proper nullability and optionality. - Enhanced default value initialization for date fields in AddCaseModal and other components to handle null values correctly. - Improved handling of date fields in ReportBuilder and DateRangePickerField to support nullable dates. * feat: enhance filtering logic for various field types in Cases component - Added support for filtering based on Integer, Number, Date, Text Long, and Text String field types. - Implemented conditions to handle cases with and without values for each field type, improving data retrieval accuracy. - Updated filterConditions logic to ensure proper handling of null and non-null values across different field types. * feat: enhance numeric filtering capabilities in Cases and ViewSelector components - Added support for filtering Integer and Number fields with new options for "No Value" and "Has Value". - Implemented operator-based filtering for numeric fields, allowing comparisons such as equals, not equals, less than, and greater than. - Updated the Cases component to handle special cases for numeric filters, improving data retrieval accuracy. - Enhanced ViewSelector to integrate new filtering options and display counts for each filter category. * feat: enhance NumericFilterInput with clear filter functionality - Added a clear filter button to the NumericFilterInput component, allowing users to reset filters easily. - Implemented display formatting for active filters using operator symbols for better user experience. - Updated ViewSelector to integrate the new clear filter functionality, improving overall filtering capabilities. * enhancement: improve date handling and validation across components - Updated date field handling in AddCase, BulkEditModal, and Cases components to use undefined instead of null for empty values, ensuring better compatibility with Zod validation. - Implemented manual validation for required date fields to address Zod v4 issues, allowing for more accurate error reporting. - Enhanced filtering logic in ViewSelector to support operator-based filtering for date fields, improving user experience and data retrieval accuracy. - Refactored default value initialization for date fields to ensure consistent handling across various components. * refactor: remove console logs from DateFilterInput component - Eliminated console log statements in the DateFilterInput component to clean up the code and improve performance. - Ensured that the filtering logic remains intact while enhancing code readability. * enhancement: enhance filtering capabilities in Cases and ViewSelector components - Implemented operator-based filtering for Text Long, Text String, Link, and Steps fields, allowing users to filter based on specific conditions. - Updated Cases component to handle new filtering logic, including post-fetch filters for text, link, and steps operators. - Enhanced ViewSelector to integrate new filter inputs for text, link, and steps, improving user experience and data retrieval accuracy. - Refactored filtering logic in useRepositoryCasesWithFilteredFields to support dynamic filtering based on user-selected criteria. * enhancement: improve filtering logic in Cases and ViewSelector components - Updated Cases component to support both numeric and string IDs for link and steps filtering, enhancing compatibility with legacy and new data formats. - Refactored filtering logic to streamline the handling of post-fetch filters and total count calculations, improving data retrieval accuracy. - Removed redundant code in ViewSelector related to link and steps filtering, simplifying the component structure and enhancing maintainability. - Enhanced useRepositoryCasesWithFilteredFields to accommodate built-in steps relation, ensuring accurate filtering based on steps count. * enhancement: integrate translation support across filter input components - Added useTranslations hook to DateFilterInput, LinkFilterInput, NumericFilterInput, StepsFilterInput, TextFilterInput, and ViewSelector components for improved localization. - Updated filter-related text to use translation keys, enhancing user experience for different locales. - Enhanced validation messages and filter prompts to be translatable, ensuring consistency across the application. * refactor: streamline data handling in Cases component and useFindManyRepositoryCasesFiltered hook - Replaced destructuring of result in Cases component with a single variable for improved clarity. - Updated useFindManyRepositoryCasesFiltered to ensure totalCount is safely accessed, enhancing robustness against undefined values. - Refactored related logic to maintain consistency and improve overall code readability. * refactor: improve type handling and exclude test-results in tsconfig - Updated tsconfig.json to exclude test-results directory for cleaner builds. - Enhanced type handling in MilestoneFormDialog and page.tsx to ensure proper undefined checks. - Adjusted resolver type in AddCase to maintain compatibility with TypeScript, improving type safety. * chore: update ioredis to 5.9.2 Updates ioredis from 5.9.1 to 5.9.2 to include bug fixes for cluster reconnection with sharded subscribers. * chore(release): 0.10.2 [skip ci] ## [0.10.2](v0.10.1...v0.10.2) (2026-01-14) ### Bug Fixes * add validation checks for data integrity in various charts ([8861224](8861224)) * enhance: Docs/update installation prerequesites (#47) * docs: update Docker and manual setup documentation with detailed RAM requirements and service dependencies - Expanded RAM requirements section in docker-setup.md to include minimum and recommended memory for building and running services. - Added per-service memory breakdown for better resource allocation guidance. - Updated manual-setup.md to clarify Node.js version requirements and categorize required and optional services for setup. * docs: enhance transaction mock methods for testCaseVersionService in bulk-edit tests - Added missing repositoryCases.findUnique() and repositoryCaseVersions.create() methods to transaction mocks for accurate test case data fetching and version creation. - Updated all relevant bulk-edit route tests to include these new mock methods, ensuring comprehensive coverage and reliability in testing. * chore(release): 0.10.3 [skip ci] ## [0.10.3](v0.10.2...v0.10.3) (2026-01-16) * chore: add ioredis dependency and update cheerio version - Added ioredis version 5.9.2 to package.json and pnpm-lock.yaml for improved Redis support. - Updated cheerio version in pnpm-lock.yaml to ensure compatibility with the latest features. * fix: update shared step group queries to exclude deleted groups - Modified queries in AddResultModal, StepsForm, StepsResults, and TestResultHistory components to filter out deleted shared step groups. - Enhanced TestCaseDetails to handle orphaned steps with a warning alert and display for steps not included in the current template. - Added translation keys for orphaned steps warning in English, Spanish, and French. * enhance: add warnings for unassigned templates and orphaned field values in TestCaseDetails - Introduced a warning alert for cases where a template is not assigned to the current project, with translations added for English, Spanish, and French. - Implemented a check for orphaned custom field values that exist outside the current template, displaying a warning and listing the orphaned fields if any are found. * fix: clear filters when switching views in ProjectRepository and update LinkFilterInput translations - Implemented logic to clear selected filters when changing to non-dynamic views and for specific field types in ProjectRepository. - Updated LinkFilterInput to correct the label for the 'domain' operator. - Revised 'noTestCases' message in English, Spanish, and French translations for clarity. * fix: ensure proper handling of orphaned field values in TestCaseDetails - Enhanced the warning alert for orphaned custom field values to provide clearer messaging when fields exist outside the current template. - Updated translations for the orphaned fields warning in English, Spanish, and French for better user experience. * fix: improve template project assignment check in TestCaseDetails - Enhanced the conditional rendering for the warning alert related to unassigned templates, ensuring it correctly checks for the presence of projects in the template. - Updated type assertions in integration tests for better type safety and clarity. --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
1 parent 721571e commit 749a172

37 files changed

Lines changed: 6675 additions & 2752 deletions

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,8 @@
6666
"@remix-run/router": ">=1.23.2",
6767
"hono": "4.11.4",
6868
"@hono/node-server": "1.19.9",
69-
"cheerio": ">=1.0.0"
69+
"cheerio": ">=1.0.0",
70+
"ioredis": "5.9.2"
7071
}
7172
}
7273
}

pnpm-lock.yaml

Lines changed: 1921 additions & 2541 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

testplanit/app/[locale]/admin/milestones/MilestoneFormDialog.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,8 @@ const FormSchema = z.object({
5252
docs: z.any().nullable(),
5353
isStarted: z.boolean(),
5454
isCompleted: z.boolean(),
55-
startedAt: z.date().optional(),
56-
completedAt: z.date().optional(),
55+
startedAt: z.date().nullable().optional(),
56+
completedAt: z.date().nullable().optional(),
5757
automaticCompletion: z.boolean(),
5858
enableNotifications: z.boolean(),
5959
notifyDaysBefore: z.number().min(0),
@@ -240,8 +240,8 @@ export const MilestoneFormDialog: React.FC<MilestoneFormDialogProps> = ({
240240
docs: docsContent,
241241
isStarted: data.isStarted,
242242
isCompleted: data.isCompleted,
243-
startedAt: data.startedAt,
244-
completedAt: data.completedAt,
243+
startedAt: data.startedAt ?? undefined,
244+
completedAt: data.completedAt ?? undefined,
245245
automaticCompletion: data.completedAt
246246
? data.automaticCompletion
247247
: false,

testplanit/app/[locale]/projects/milestones/[projectId]/AddMilestoneModal.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,8 @@ const FormSchema = z.object({
6060
docs: z.any().nullable(),
6161
isStarted: z.boolean(),
6262
isCompleted: z.boolean(),
63-
startedAt: z.date().optional(),
64-
completedAt: z.date().optional(),
63+
startedAt: z.date().nullable().optional(),
64+
completedAt: z.date().nullable().optional(),
6565
automaticCompletion: z.boolean(),
6666
enableNotifications: z.boolean(),
6767
notifyDaysBefore: z.number().min(0),

testplanit/app/[locale]/projects/repository/[projectId]/AddCase.tsx

Lines changed: 165 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import {
1818
} from "~/lib/hooks";
1919
import { useProjectPermissions } from "~/hooks/useProjectPermissions";
2020
import { zodResolver } from "@hookform/resolvers/zod";
21-
import { useForm, Controller, Resolver } from "react-hook-form";
21+
import { useForm, Controller } from "react-hook-form";
2222
import { z } from "zod/v4";
2323
import { Button } from "@/components/ui/button";
2424
import { Input } from "@/components/ui/input";
@@ -103,23 +103,66 @@ const mapFieldToZodType = (field: any) => {
103103
? z.boolean().prefault(field.caseField.isChecked)
104104
: z.boolean().prefault(field.caseField.isChecked).optional();
105105
case "Date":
106-
return isRequired ? z.date() : z.date().optional();
106+
// Use z.any() to skip Zod validation - we'll handle nulls via resolver transformation
107+
return z.any();
107108
case "Multi-Select":
108109
return isRequired ? z.number().array() : z.number().array().optional();
109110
case "Dropdown":
110111
return isRequired ? z.number() : z.number().optional();
111112
case "Integer":
112-
const integerSchema = z.int();
113-
return isRequired
114-
? addMinMax(integerSchema)
115-
: addMinMax(integerSchema).optional();
113+
let integerBaseSchema = z.union([
114+
z.number().int(),
115+
z.string().transform((val) => (val === "" ? undefined : parseInt(val, 10))),
116+
]);
117+
118+
// Apply min/max constraints using refine
119+
if (field.caseField.minValue !== undefined && field.caseField.minValue !== null) {
120+
const minValue = field.caseField.minValue;
121+
integerBaseSchema = integerBaseSchema.refine(
122+
(val) => val === undefined || (typeof val === 'number' && val >= minValue),
123+
{ message: `Value must be at least ${minValue}` }
124+
) as any;
125+
}
126+
if (field.caseField.maxValue !== undefined && field.caseField.maxValue !== null) {
127+
const maxValue = field.caseField.maxValue;
128+
integerBaseSchema = integerBaseSchema.refine(
129+
(val) => val === undefined || (typeof val === 'number' && val <= maxValue),
130+
{ message: `Value must be at most ${maxValue}` }
131+
) as any;
132+
}
133+
134+
return isRequired ? integerBaseSchema : integerBaseSchema.optional();
135+
116136
case "Number":
117-
const numberSchema = z.number();
118-
return isRequired
119-
? addMinMax(numberSchema)
120-
: addMinMax(numberSchema).optional();
137+
let numberBaseSchema = z.union([
138+
z.number(),
139+
z.string().transform((val) => (val === "" ? undefined : parseFloat(val))),
140+
]);
141+
142+
// Apply min/max constraints using refine
143+
if (field.caseField.minValue !== undefined && field.caseField.minValue !== null) {
144+
const minValue = field.caseField.minValue;
145+
numberBaseSchema = numberBaseSchema.refine(
146+
(val) => val === undefined || (typeof val === 'number' && val >= minValue),
147+
{ message: `Value must be at least ${minValue}` }
148+
) as any;
149+
}
150+
if (field.caseField.maxValue !== undefined && field.caseField.maxValue !== null) {
151+
const maxValue = field.caseField.maxValue;
152+
numberBaseSchema = numberBaseSchema.refine(
153+
(val) => val === undefined || (typeof val === 'number' && val <= maxValue),
154+
{ message: `Value must be at most ${maxValue}` }
155+
) as any;
156+
}
157+
158+
return isRequired ? numberBaseSchema : numberBaseSchema.optional();
121159
case "Link":
122-
return isRequired ? z.url() : z.url().optional();
160+
return isRequired
161+
? z.string().url()
162+
: z.union([
163+
z.string().url(),
164+
z.literal(""),
165+
]).optional();
123166
case "Text String":
124167
return isRequired ? z.string() : z.string().optional();
125168
case "Text Long":
@@ -202,7 +245,10 @@ const createFormSchema = (fields: any[]) => {
202245
const dynamicSchema = fields.reduce(
203246
(schema, field) => {
204247
const fieldName = field.caseField.id.toString();
205-
schema[fieldName] = mapFieldToZodType(field);
248+
// Skip Date fields entirely - we'll handle them manually without validation
249+
if (field.caseField.type.type !== "Date") {
250+
schema[fieldName] = mapFieldToZodType(field);
251+
}
206252
return schema;
207253
},
208254
{} as Record<string, z.ZodTypeAny>
@@ -392,7 +438,8 @@ export function AddCaseModal({ folderId }: AddCaseModalProps) {
392438
),
393439
})) || [];
394440
const form = useForm<FormValues>({
395-
resolver: zodResolver(formSchema) as unknown as Resolver<FormValues>,
441+
resolver: zodResolver(formSchema) as any,
442+
mode: 'onSubmit',
396443
defaultValues: {
397444
name: "",
398445
templateId: defaultTemplateId ?? 0,
@@ -520,18 +567,43 @@ export function AddCaseModal({ folderId }: AddCaseModalProps) {
520567
};
521568
selectedTemplate.caseFields.forEach((caseField: any) => {
522569
const fieldIdStr = caseField.caseField.id.toString();
523-
if (
524-
caseField.caseField.type.type === "Dropdown" &&
525-
caseField.caseField.fieldOptions
526-
) {
527-
const defaultOption = caseField.caseField.fieldOptions.find(
528-
(option: any) => option.fieldOption.isDefault
529-
);
530-
if (defaultOption) {
531-
defaultValues[fieldIdStr] = defaultOption.fieldOption.id;
532-
}
533-
} else if (caseField.caseField.type.type === "Steps") {
534-
defaultValues[fieldIdStr] = [];
570+
const fieldType = caseField.caseField.type.type;
571+
572+
// Initialize all field types with appropriate defaults
573+
switch (fieldType) {
574+
case "Dropdown":
575+
if (caseField.caseField.fieldOptions) {
576+
const defaultOption = caseField.caseField.fieldOptions.find(
577+
(option: any) => option.fieldOption.isDefault
578+
);
579+
if (defaultOption) {
580+
defaultValues[fieldIdStr] = defaultOption.fieldOption.id;
581+
}
582+
}
583+
break;
584+
case "Multi-Select":
585+
defaultValues[fieldIdStr] = [];
586+
break;
587+
case "Steps":
588+
defaultValues[fieldIdStr] = [];
589+
break;
590+
case "Integer":
591+
case "Number":
592+
defaultValues[fieldIdStr] = "";
593+
break;
594+
case "Date":
595+
defaultValues[fieldIdStr] = undefined;
596+
break;
597+
case "Checkbox":
598+
defaultValues[fieldIdStr] = caseField.caseField.isChecked ?? false;
599+
break;
600+
case "Link":
601+
case "Text String":
602+
defaultValues[fieldIdStr] = "";
603+
break;
604+
case "Text Long":
605+
defaultValues[fieldIdStr] = JSON.stringify(emptyEditorContent);
606+
break;
535607
}
536608
});
537609
reset(defaultValues as FormValues);
@@ -561,17 +633,22 @@ export function AddCaseModal({ folderId }: AddCaseModalProps) {
561633

562634
if (selectedTemplate) {
563635
selectedTemplate.caseFields.forEach((caseField: any) => {
564-
if (
565-
caseField.caseField.type.type === "Dropdown" &&
566-
caseField.caseField.fieldOptions
567-
) {
636+
const fieldIdStr = caseField.caseField.id.toString();
637+
const fieldType = caseField.caseField.type.type;
638+
639+
if (fieldType === "Dropdown" && caseField.caseField.fieldOptions) {
568640
const defaultOption = caseField.caseField.fieldOptions.find(
569641
(option: any) => option.fieldOption.isDefault
570642
);
571643
if (defaultOption) {
572-
defaultValues[caseField.caseField.id.toString()] =
573-
defaultOption.fieldOption.id;
644+
defaultValues[fieldIdStr] = defaultOption.fieldOption.id;
574645
}
646+
} else if (fieldType === "Steps") {
647+
defaultValues[fieldIdStr] = [];
648+
} else if (fieldType === "Integer" || fieldType === "Number") {
649+
defaultValues[fieldIdStr] = "";
650+
} else if (fieldType === "Date") {
651+
defaultValues[fieldIdStr] = undefined;
575652
}
576653
});
577654
// Enable the name field since we have a template selected and loaded
@@ -606,19 +683,44 @@ export function AddCaseModal({ folderId }: AddCaseModalProps) {
606683
automated: false,
607684
};
608685
selectedTemplate.caseFields.forEach((caseField: any) => {
609-
if (
610-
caseField.caseField.type.type === "Dropdown" &&
611-
caseField.caseField.fieldOptions
612-
) {
613-
const defaultOption = caseField.caseField.fieldOptions.find(
614-
(option: any) => option.fieldOption.isDefault
615-
);
616-
if (defaultOption) {
617-
defaultValues[caseField.caseField.id.toString()] =
618-
defaultOption.fieldOption.id;
619-
}
620-
} else if (caseField.caseField.type.type === "Steps") {
621-
defaultValues[caseField.caseField.id.toString()] = [];
686+
const fieldIdStr = caseField.caseField.id.toString();
687+
const fieldType = caseField.caseField.type.type;
688+
689+
// Initialize all field types with appropriate defaults
690+
switch (fieldType) {
691+
case "Dropdown":
692+
if (caseField.caseField.fieldOptions) {
693+
const defaultOption = caseField.caseField.fieldOptions.find(
694+
(option: any) => option.fieldOption.isDefault
695+
);
696+
if (defaultOption) {
697+
defaultValues[fieldIdStr] = defaultOption.fieldOption.id;
698+
}
699+
}
700+
break;
701+
case "Multi-Select":
702+
defaultValues[fieldIdStr] = [];
703+
break;
704+
case "Steps":
705+
defaultValues[fieldIdStr] = [];
706+
break;
707+
case "Integer":
708+
case "Number":
709+
defaultValues[fieldIdStr] = "";
710+
break;
711+
case "Date":
712+
defaultValues[fieldIdStr] = undefined;
713+
break;
714+
case "Checkbox":
715+
defaultValues[fieldIdStr] = caseField.caseField.isChecked ?? false;
716+
break;
717+
case "Link":
718+
case "Text String":
719+
defaultValues[fieldIdStr] = "";
720+
break;
721+
case "Text Long":
722+
defaultValues[fieldIdStr] = JSON.stringify(emptyEditorContent);
723+
break;
622724
}
623725
});
624726
reset(defaultValues as FormValues);
@@ -632,6 +734,25 @@ export function AddCaseModal({ folderId }: AddCaseModalProps) {
632734
async function onSubmit(data: FormValues) {
633735
setIsSubmitting(true);
634736

737+
// Manual validation for required date fields (since we excluded them from Zod schema)
738+
const selectedTemplate = templates?.find(t => t.id === selectedTemplateId);
739+
if (selectedTemplate) {
740+
for (const fieldMeta of selectedTemplate.caseFields) {
741+
if (fieldMeta.caseField.type.type === "Date" && fieldMeta.caseField.isRequired) {
742+
const fieldIdStr = fieldMeta.caseField.id.toString();
743+
const value = data[fieldIdStr];
744+
if (!value || !(value instanceof Date) || isNaN(value.getTime())) {
745+
form.setError(fieldIdStr, {
746+
type: 'manual',
747+
message: `${fieldMeta.caseField.displayName} is required`,
748+
});
749+
setIsSubmitting(false);
750+
return;
751+
}
752+
}
753+
}
754+
}
755+
635756
try {
636757
if (session) {
637758
const convertedData: FormValues = {

testplanit/app/[locale]/projects/repository/[projectId]/AddResultModal.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2160,7 +2160,10 @@ const SharedStepGroupInputs: React.FC<SharedStepGroupInputsProps> = ({
21602160
const t = useTranslations();
21612161
const tCommon = useTranslations("common");
21622162
const { data: items, isLoading } = useFindManySharedStepItem({
2163-
where: { sharedStepGroupId },
2163+
where: {
2164+
sharedStepGroupId,
2165+
sharedStepGroup: { isDeleted: false },
2166+
},
21642167
orderBy: { order: "asc" },
21652168
});
21662169
const queryClient = useQueryClient(); // Added for use in useEffect to setQueryData

testplanit/app/[locale]/projects/repository/[projectId]/BulkEditModal.tsx

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1053,6 +1053,18 @@ export function BulkEditModal({
10531053
const fieldDef = allFieldDefinitions.find((f) => f.key === fieldKey);
10541054
if (!fieldDef) continue; // Should not happen if data is consistent
10551055

1056+
// Skip Zod validation for Date fields entirely due to Zod v4 bug with null dates
1057+
// Perform manual validation for required Date fields instead
1058+
if (fieldDef.isCustom && fieldDef.field?.type?.type === "Date") {
1059+
const isRequired = fieldDef.field?.isRequired ?? false;
1060+
if (isRequired) {
1061+
if (!value || !(value instanceof Date) || isNaN(value.getTime())) {
1062+
errors[fieldKey] = [`${fieldDef.label} is required`];
1063+
}
1064+
}
1065+
continue; // Skip normal Zod validation for Date fields
1066+
}
1067+
10561068
let schema: z.ZodTypeAny = z.any(); // Initialize schema
10571069
const isRequired = fieldDef.isCustom
10581070
? (fieldDef.field?.isRequired ?? false)
@@ -1123,8 +1135,8 @@ export function BulkEditModal({
11231135
// Required for checkbox doesn't make much sense unless it must be true
11241136
break;
11251137
case "Date":
1126-
const dateSchema = z.date();
1127-
schema = isRequired ? dateSchema : dateSchema.nullable();
1138+
// Use z.any() to skip Zod validation - we'll handle nulls via resolver transformation
1139+
schema = z.any();
11281140
break;
11291141
case "Multi-Select":
11301142
let multiSchema = z.array(z.number());

0 commit comments

Comments
 (0)