@checkstack/ui 1.1.5 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,59 @@
1
1
  # @checkstack/ui
2
2
 
3
+ ## 1.2.1
4
+
5
+ ### Patch Changes
6
+
7
+ - d1a2796: Enforce stricter code quality standards and eliminate AI slop anti-patterns.
8
+
9
+ **New utility**
10
+
11
+ - `extractErrorMessage(error, fallback?)` in `@checkstack/common` for consistent error extraction
12
+
13
+ **ESLint rules**
14
+
15
+ - `react-hooks/rules-of-hooks` and `exhaustive-deps` for hook correctness
16
+ - `no-console` in frontend packages — forces `toast` over silent `console.error`
17
+ - `no-restricted-syntax` banning `instanceof Error` — forces `extractErrorMessage`
18
+ - Custom `no-eslint-disable-any` rule preventing `@typescript-eslint/no-explicit-any` circumvention
19
+
20
+ **Refactoring**
21
+
22
+ - Replace 141 `instanceof Error` boilerplate patterns across the codebase
23
+ - Replace swallowed `console.error` with user-visible `toast.error()` feedback
24
+ - Remove 15 redundant `as` type casts in IntegrationsPage and ProviderConnectionsPage
25
+ - Consolidate 3 identical callback handlers into `handleDialogClose`
26
+ - Fix conditional React hook call in `FormField.tsx`
27
+ - Fix unstable useMemo deps in `Dashboard.tsx`
28
+ - Replace `useEffect`→`setState` with derived `useMemo` in `RegisterPage.tsx`
29
+ - Rewrite `keystore.test.ts` with typed `DrizzleMockChain` (eliminating 7 `any` suppressions)
30
+ - Delete obvious comments in `encryption.ts` and Teams `provider.ts`
31
+
32
+ - Updated dependencies [d1a2796]
33
+ - @checkstack/common@0.6.5
34
+ - @checkstack/frontend-api@0.3.9
35
+
36
+ ## 1.2.0
37
+
38
+ ### Minor Changes
39
+
40
+ - 23c80bc: ### Jira Data Center Support
41
+
42
+ Added support for on-premise Jira Data Center installations alongside existing Jira Cloud support:
43
+
44
+ - **Authentication mode switching**: New `authMode` field (`cloud` | `datacenter`) on connection configuration. Cloud uses Basic Auth (email + API token), Data Center uses Bearer Auth (Personal Access Token).
45
+ - **API version routing**: Automatically selects REST API v3 for Cloud and v2 for Data Center.
46
+ - **Description format**: Cloud uses Atlassian Document Format (ADF), Data Center uses plain text.
47
+ - **Connection schema v2**: Backward-compatible — defaults to `cloud` mode for existing connections.
48
+
49
+ ### DynamicForm `x-hidden-when` Conditional Visibility
50
+
51
+ New generic platform feature for conditionally hiding form fields based on sibling field values:
52
+
53
+ - Added `x-hidden-when` metadata extension to `ConfigMeta` and `JsonSchemaProperty`.
54
+ - DynamicForm automatically hides fields and skips their validation when conditions match.
55
+ - Used by Jira integration to hide the email field when `authMode` is `datacenter`.
56
+
3
57
  ## 1.1.5
4
58
 
5
59
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@checkstack/ui",
3
- "version": "1.1.5",
3
+ "version": "1.2.1",
4
4
  "type": "module",
5
5
  "main": "src/index.ts",
6
6
  "dependencies": {
@@ -31,7 +31,7 @@
31
31
  "@types/react": "^18.2.0",
32
32
  "@testing-library/react": "^16.0.0",
33
33
  "@checkstack/test-utils-frontend": "0.0.4",
34
- "@checkstack/tsconfig": "0.0.4",
34
+ "@checkstack/tsconfig": "0.0.5",
35
35
  "@checkstack/scripts": "0.1.2"
36
36
  },
37
37
  "scripts": {
@@ -3,7 +3,7 @@ import React from "react";
3
3
  import { EmptyState } from "../../index";
4
4
 
5
5
  import type { DynamicFormProps } from "./types";
6
- import { extractDefaults, isValueEmpty } from "./utils";
6
+ import { extractDefaults, isValueEmpty, isFieldHiddenByCondition } from "./utils";
7
7
  import { FormField } from "./FormField";
8
8
 
9
9
  /**
@@ -33,7 +33,8 @@ export const DynamicForm: React.FC<DynamicFormProps> = ({
33
33
  if (JSON.stringify(merged) !== JSON.stringify(value)) {
34
34
  onChange(merged);
35
35
  }
36
- }, [schema]); // Only run when schema changes
36
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- Intentional: runs only on schema change. Including onChange would re-fire on parent re-renders; including value would cause an infinite loop since this effect calls onChange(merged)
37
+ }, [schema]);
37
38
 
38
39
  // Compute validity and report changes
39
40
  React.useEffect(() => {
@@ -50,6 +51,13 @@ export const DynamicForm: React.FC<DynamicFormProps> = ({
50
51
  // Skip hidden fields - they are auto-populated
51
52
  if (propSchema["x-hidden"]) continue;
52
53
 
54
+ // Skip conditionally hidden fields - they are not visible
55
+ if (
56
+ propSchema["x-hidden-when"] &&
57
+ isFieldHiddenByCondition(propSchema["x-hidden-when"], value)
58
+ )
59
+ continue;
60
+
53
61
  if (isValueEmpty(value[key], propSchema)) {
54
62
  isValid = false;
55
63
  break;
@@ -79,7 +87,15 @@ export const DynamicForm: React.FC<DynamicFormProps> = ({
79
87
  return (
80
88
  <div className="space-y-6">
81
89
  {Object.entries(schema.properties)
82
- .filter(([, propSchema]) => !propSchema["x-hidden"])
90
+ .filter(([, propSchema]) => {
91
+ if (propSchema["x-hidden"]) return false;
92
+ if (
93
+ propSchema["x-hidden-when"] &&
94
+ isFieldHiddenByCondition(propSchema["x-hidden-when"], value)
95
+ )
96
+ return false;
97
+ return true;
98
+ })
83
99
  .map(([key, propSchema]) => {
84
100
  const isRequired = schema.required?.includes(key);
85
101
  const label = key.charAt(0).toUpperCase() + key.slice(1);
@@ -13,6 +13,7 @@ import {
13
13
 
14
14
  import type { DynamicOptionsFieldProps, ResolverOption } from "./types";
15
15
  import { getCleanDescription, NONE_SENTINEL } from "./utils";
16
+ import { extractErrorMessage } from "@checkstack/common";
16
17
 
17
18
  /**
18
19
  * Field component for dynamically resolved options.
@@ -72,7 +73,7 @@ export const DynamicOptionsField: React.FC<DynamicOptionsFieldProps> = ({
72
73
  .catch((error_) => {
73
74
  if (!cancelled) {
74
75
  setError(
75
- error_ instanceof Error ? error_.message : "Failed to load options",
76
+ extractErrorMessage(error_, "Failed to load options"),
76
77
  );
77
78
  setLoading(false);
78
79
  }
@@ -1,4 +1,4 @@
1
- import React from "react";
1
+ import { useEffect, useState } from "react";
2
2
  import { Plus, Trash2 } from "lucide-react";
3
3
 
4
4
  import {
@@ -37,6 +37,14 @@ export const FormField: React.FC<FormFieldProps> = ({
37
37
  }) => {
38
38
  const description = propSchema.description || "";
39
39
 
40
+ // Const field handling - must be before any early returns (rules-of-hooks)
41
+ const isConstField = propSchema.const !== undefined;
42
+ useEffect(() => {
43
+ if (isConstField && value !== propSchema.const) {
44
+ onChange(propSchema.const);
45
+ }
46
+ }, [isConstField, value, propSchema.const, onChange]);
47
+
40
48
  // Dynamic options via resolver
41
49
  const resolverName = propSchema["x-options-resolver"];
42
50
  if (resolverName && optionsResolvers) {
@@ -57,14 +65,7 @@ export const FormField: React.FC<FormFieldProps> = ({
57
65
  );
58
66
  }
59
67
 
60
- // Const field handling - auto-set value and hide (value is fixed)
61
- if (propSchema.const !== undefined) {
62
- // Silently ensure the value is set, no UI needed
63
- React.useEffect(() => {
64
- if (value !== propSchema.const) {
65
- onChange(propSchema.const);
66
- }
67
- }, [value, propSchema.const, onChange]);
68
+ if (isConstField) {
68
69
  return <></>;
69
70
  }
70
71
 
@@ -653,7 +654,7 @@ const SecretField: React.FC<{
653
654
  isRequired?: boolean;
654
655
  onChange: (val: unknown) => void;
655
656
  }> = ({ id, label, description, value, isRequired, onChange }) => {
656
- const [showPassword, setShowPassword] = React.useState(false);
657
+ const [showPassword, setShowPassword] = useState(false);
657
658
  const currentValue = value || "";
658
659
  const hasExistingValue = currentValue.length > 0;
659
660
 
@@ -702,7 +703,7 @@ const SecretTextareaField: React.FC<{
702
703
  isRequired?: boolean;
703
704
  onChange: (val: unknown) => void;
704
705
  }> = ({ id, label, description, value, isRequired, onChange }) => {
705
- const [showContent, setShowContent] = React.useState(false);
706
+ const [showContent, setShowContent] = useState(false);
706
707
  const currentValue = value || "";
707
708
  const hasExistingValue = currentValue.length > 0;
708
709
 
@@ -723,7 +724,9 @@ const SecretTextareaField: React.FC<{
723
724
  value={currentValue}
724
725
  onChange={(e) => onChange(e.target.value)}
725
726
  placeholder={
726
- hasExistingValue ? "Leave empty to keep existing value" : "Paste content here"
727
+ hasExistingValue
728
+ ? "Leave empty to keep existing value"
729
+ : "Paste content here"
727
730
  }
728
731
  rows={5}
729
732
  className="pr-10 font-mono text-xs"
@@ -750,7 +753,9 @@ const SecretTextareaField: React.FC<{
750
753
  }
751
754
  }}
752
755
  placeholder={
753
- hasExistingValue ? "Leave empty to keep existing value" : "Paste content here"
756
+ hasExistingValue
757
+ ? "Leave empty to keep existing value"
758
+ : "Paste content here"
754
759
  }
755
760
  rows={3}
756
761
  className="pr-10"
@@ -21,6 +21,7 @@ export interface JsonSchemaProperty extends JsonSchemaPropertyCore<JsonSchemaPro
21
21
  "x-hidden"?: boolean; // Field should be hidden in form (auto-populated)
22
22
  "x-searchable"?: boolean; // Shows search input for filtering dropdown options
23
23
  "x-editor-types"?: EditorType[]; // Available editor types for multi-type input
24
+ "x-hidden-when"?: Record<string, string[]>; // Conditionally hide based on sibling field values
24
25
  }
25
26
 
26
27
  /** Option returned by an options resolver */
@@ -5,6 +5,7 @@ import {
5
5
  extractDefaults,
6
6
  getCleanDescription,
7
7
  isValueEmpty,
8
+ isFieldHiddenByCondition,
8
9
  NONE_SENTINEL,
9
10
  parseSelectValue,
10
11
  serializeFormData,
@@ -398,3 +399,71 @@ describe("detectEditorType", () => {
398
399
  });
399
400
  });
400
401
  });
402
+
403
+ // =============================================================================
404
+ // Conditional Visibility Tests
405
+ // =============================================================================
406
+
407
+ describe("isFieldHiddenByCondition", () => {
408
+ it("returns true when sibling value matches a value in the array", () => {
409
+ const conditions = { authMode: ["datacenter"] };
410
+ const formValues = { authMode: "datacenter" };
411
+ expect(isFieldHiddenByCondition(conditions, formValues)).toBe(true);
412
+ });
413
+
414
+ it("returns true when sibling value matches any value in the array", () => {
415
+ const conditions = { authMode: ["datacenter", "server"] };
416
+ const formValues = { authMode: "server" };
417
+ expect(isFieldHiddenByCondition(conditions, formValues)).toBe(true);
418
+ });
419
+
420
+ it("returns false when sibling value does not match", () => {
421
+ const conditions = { authMode: ["datacenter"] };
422
+ const formValues = { authMode: "cloud" };
423
+ expect(isFieldHiddenByCondition(conditions, formValues)).toBe(false);
424
+ });
425
+
426
+ it("returns false when sibling field is missing (coerces to empty string)", () => {
427
+ const conditions = { authMode: ["datacenter"] };
428
+ const formValues = {};
429
+ expect(isFieldHiddenByCondition(conditions, formValues)).toBe(false);
430
+ });
431
+
432
+ it("returns true when sibling field is missing and empty string is in the value list", () => {
433
+ const conditions = { authMode: [""] };
434
+ const formValues = {};
435
+ expect(isFieldHiddenByCondition(conditions, formValues)).toBe(true);
436
+ });
437
+
438
+ it("handles multiple conditions with OR semantics (any match hides)", () => {
439
+ const conditions = {
440
+ authMode: ["datacenter"],
441
+ environment: ["staging"],
442
+ };
443
+ // Only authMode matches
444
+ expect(
445
+ isFieldHiddenByCondition(conditions, {
446
+ authMode: "datacenter",
447
+ environment: "production",
448
+ }),
449
+ ).toBe(true);
450
+ // Only environment matches
451
+ expect(
452
+ isFieldHiddenByCondition(conditions, {
453
+ authMode: "cloud",
454
+ environment: "staging",
455
+ }),
456
+ ).toBe(true);
457
+ // Neither matches
458
+ expect(
459
+ isFieldHiddenByCondition(conditions, {
460
+ authMode: "cloud",
461
+ environment: "production",
462
+ }),
463
+ ).toBe(false);
464
+ });
465
+
466
+ it("returns false for empty conditions object", () => {
467
+ expect(isFieldHiddenByCondition({}, { authMode: "cloud" })).toBe(false);
468
+ });
469
+ });
@@ -72,6 +72,22 @@ export function isValueEmpty(
72
72
  /** Sentinel value used to represent "None" selection in Select components */
73
73
  export const NONE_SENTINEL = "__none__";
74
74
 
75
+ /**
76
+ * Evaluate x-hidden-when conditions against current form values.
77
+ * Returns true if the field should be hidden.
78
+ *
79
+ * Each condition maps a sibling field name to values that trigger hiding.
80
+ * The field is hidden if ANY condition matches (OR semantics).
81
+ */
82
+ export function isFieldHiddenByCondition(
83
+ conditions: Record<string, string[]>,
84
+ formValues: Record<string, unknown>,
85
+ ): boolean {
86
+ return Object.entries(conditions).some(([field, values]) =>
87
+ values.includes(String(formValues[field] ?? "")),
88
+ );
89
+ }
90
+
75
91
  /**
76
92
  * Converts a select value to the actual form value.
77
93
  * Handles the "None" sentinel value by returning undefined.