Skip to content

Commit e811f7a

Browse files
committed
wip
1 parent 6f93170 commit e811f7a

File tree

4 files changed

+122
-49
lines changed

4 files changed

+122
-49
lines changed

flagsmith-jira-app/src/backend/flagsmith.ts

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ type PaginatedModels<TModel extends Model> = {
1717

1818
export type Organisation = Model;
1919
export type Project = Model;
20-
export type Environment = Model & { api_key: string };
20+
export type Environment = Model & { api_key: string; project: number };
2121

2222
export type Feature = Model & {
2323
id: number | string;
@@ -27,6 +27,8 @@ export type Feature = Model & {
2727
multivariate_options: unknown[];
2828
num_segment_overrides: number | null;
2929
num_identity_overrides: number | null;
30+
project: number;
31+
projectName: string;
3032
};
3133

3234
export type EnvironmentFeatureState = Model & {
@@ -171,6 +173,8 @@ export const readFeatures: ReadFeatures = async ({ projectIds, environmentId })
171173
}
172174

173175
const allFeatures: Feature[] = [];
176+
const organisationId = await readOrganisationId();
177+
const projects = await readProjects({ organisationId });
174178

175179
for (const projectId of projectIds) {
176180
const params = new URLSearchParams({ is_archived: "false" });
@@ -180,7 +184,19 @@ export const readFeatures: ReadFeatures = async ({ projectIds, environmentId })
180184
try {
181185
const data = (await flagsmithApi(apiKey, path)) as PaginatedModels<Feature>;
182186
const results = await unpaginate(apiKey, data);
183-
allFeatures.push(...results);
187+
188+
const project = projects.find((p) => String(p.id) === String(projectId));
189+
190+
if (!project) {
191+
throw new ApiError(`Flagsmith project ${projectId} not found`, 404);
192+
}
193+
194+
const enrichedFeatures = results.map((feature) => ({
195+
...feature,
196+
projectName: project.name,
197+
}));
198+
199+
allFeatures.push(...enrichedFeatures);
184200
} catch (error) {
185201
// If a specific project has no features, we can log it but continue with others
186202
if (error instanceof ApiError && error.code === 404) {
@@ -198,19 +214,6 @@ export const readFeatures: ReadFeatures = async ({ projectIds, environmentId })
198214
return allFeatures;
199215
};
200216

201-
// export const readFeatures: ReadFeatures = async ({ projectIds, environmentId }) => {
202-
// const apiKey = await readApiKey();
203-
// checkApiKey(apiKey);
204-
// if (!projectIds) throw new ApiError("Flagsmith project not connected", 400);
205-
// const params = new URLSearchParams({ is_archived: "false" });
206-
// if (environmentId) params.set("environment", environmentId);
207-
// const path = route`/projects/${projectId}/features/?${params}`;
208-
// const data = (await flagsmithApi(apiKey, path)) as PaginatedModels<Feature>;
209-
// const results = await unpaginate(apiKey, data);
210-
// if (results.length === 0) throw new ApiError("Flagsmith project has no features", 404);
211-
// return results;
212-
// };
213-
214217
export type ReadEnvironmentFeatureState = (args: {
215218
envApiKey: string;
216219
featureName: string;
@@ -224,6 +227,7 @@ export const readEnvironmentFeatureState: ReadEnvironmentFeatureState = async ({
224227
const apiKey = await readApiKey();
225228
checkApiKey(apiKey);
226229
if (!envApiKey) throw new ApiError("Flagsmith environment not connected", 400);
230+
227231
const path = route`/environments/${envApiKey}/featurestates/?feature_name=${featureName}`;
228232
const data = (await flagsmithApi(apiKey, path)) as PaginatedModels<EnvironmentFeatureState>;
229233
const results = await unpaginate(apiKey, data);

flagsmith-jira-app/src/frontend/components/IssueFeatureTables.tsx

Lines changed: 60 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -151,29 +151,47 @@ const IssueFeatureTable = ({
151151
const projectUrl = `${FLAGSMITH_APP}/project/${projectIds[0]}`; // TODO
152152

153153
/** Read feature state for each environment */
154-
const readFeatureState = useCallback(
155-
async (): Promise<FeatureState> => ({
154+
const readFeatureState = useCallback(async (): Promise<FeatureState> => {
155+
const featureProjectId = environmentFeatures[0]?.project;
156+
157+
const environmentFeatureStates = await Promise.all(
158+
environments
159+
.filter((env) => String(env.project) === String(featureProjectId))
160+
.map(async (environment) => {
161+
return {
162+
...(await readEnvironmentFeatureState({
163+
envApiKey: String(environment.api_key),
164+
featureName,
165+
})),
166+
name: environment.name,
167+
api_key: String(environment.api_key),
168+
};
169+
}),
170+
);
171+
172+
return {
156173
featureId,
157-
environments: await Promise.all(
158-
environments.map(async (environment) => ({
159-
...(await readEnvironmentFeatureState({
160-
envApiKey: String(environment.api_key),
161-
featureName,
162-
})),
163-
name: environment.name,
164-
api_key: String(environment.api_key),
165-
})),
166-
),
174+
environments: environmentFeatureStates,
167175
counts: environmentFeatures.map((feature) => ({
168176
variations: feature.multivariate_options.length,
169177
segments: feature.num_segment_overrides ?? 0,
170178
identities: feature.num_identity_overrides ?? 0,
171179
})),
172-
}),
173-
[environments, environmentFeatures],
174-
);
180+
};
181+
}, [environments, environmentFeatures]);
182+
175183
const [state] = usePromise(
176-
async () => (error === undefined ? readFeatureState() : undefined),
184+
async () => {
185+
if (error === undefined) {
186+
try {
187+
return await readFeatureState();
188+
} catch (err) {
189+
console.error("[ERROR] Failed to read feature state:", err);
190+
throw err;
191+
}
192+
}
193+
return undefined;
194+
},
177195
[error, readFeatureState],
178196
setError,
179197
);
@@ -227,17 +245,36 @@ const IssueFeatureTables = ({
227245
return (
228246
<Fragment>
229247
{issueFeatureIds.map((featureId) => {
248+
console.log("[DEBUG] Processing featureId:", featureId);
249+
230250
const baseFeature = features.find((f) => String(f.id) === featureId);
231251
if (!baseFeature) {
252+
console.warn("[WARN] baseFeature not found for featureId:", featureId);
232253
return null;
233254
}
234255

235-
const envFeaturesForThisFeature = environmentsFeatures
236-
.map((envFeatures) => {
237-
const matchingFeature = envFeatures.find((f) => String(f.id) === featureId);
238-
return matchingFeature;
239-
})
240-
.filter(Boolean) as Feature[];
256+
const envFeaturesForThisFeature: Feature[] = [];
257+
const matchingEnvironments: Environment[] = [];
258+
259+
environmentsFeatures.forEach((envFeatures, index) => {
260+
const matchingFeature = envFeatures.find((f) => String(f.id) === featureId);
261+
const environment = environments[index];
262+
if (matchingFeature && environment !== undefined) {
263+
envFeaturesForThisFeature.push(matchingFeature);
264+
matchingEnvironments.push(environment);
265+
}
266+
});
267+
268+
console.log(
269+
`[DEBUG] FeatureId: ${featureId} — envFeaturesForThisFeature.length: ${envFeaturesForThisFeature.length}, matchingEnvironments.length: ${matchingEnvironments.length}`,
270+
);
271+
272+
if (envFeaturesForThisFeature.length === 0 || matchingEnvironments.length === 0) {
273+
console.warn(
274+
`[WARN] Skipping featureId ${featureId} — no matching features or environments`,
275+
);
276+
return null;
277+
}
241278

242279
return (
243280
<Fragment key={featureId}>
@@ -252,7 +289,7 @@ const IssueFeatureTables = ({
252289
</Box>
253290
<IssueFeatureTable
254291
projectIds={projectIds}
255-
environments={environments}
292+
environments={matchingEnvironments}
256293
environmentFeatures={envFeaturesForThisFeature}
257294
/>
258295
</Fragment>

flagsmith-jira-app/src/frontend/components/IssueFeaturesForm.tsx

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,15 +27,32 @@ const IssueFeaturesForm = ({
2727
}, [props.issueFeatureIds]);
2828

2929
const featureInputId = useId();
30-
const featureOptions = useMemo(
31-
() =>
32-
features.map((each) => ({
33-
label: each.name, // might need to change this to localise for a project, or include description?
34-
value: String(each.id),
35-
})),
36-
[features],
37-
);
38-
const featureValue = featureOptions.filter((option) => featureIds.includes(option.value)) ?? null;
30+
const featureOptions = useMemo(() => {
31+
const grouped = features.reduce(
32+
(acc, feature) => {
33+
const projectGroup = acc.find((group) => group.label === feature.projectName);
34+
const option = {
35+
label: feature.name,
36+
value: String(feature.id),
37+
};
38+
if (projectGroup) {
39+
projectGroup.options.push(option);
40+
} else {
41+
acc.push({
42+
label: feature.projectName,
43+
options: [option],
44+
});
45+
}
46+
return acc;
47+
},
48+
[] as { label: string; options: { label: string; value: string }[] }[],
49+
);
50+
return grouped;
51+
}, [features]);
52+
const featureValue =
53+
featureOptions
54+
.flatMap((group) => group.options)
55+
.filter((option) => featureIds.includes(option.value)) ?? null;
3956

4057
const onFeatureChange = async (options: { value: string }[]) => {
4158
const featureIds = options.map((option) => option.value);

flagsmith-jira-app/src/frontend/jira.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,22 @@ const FEATURE_IDS = "flagsmith.features";
8686
export const readProjectIds = async (extension: ExtensionData): Promise<string[]> => {
8787
const entityType = "project";
8888
const entityId = String(extension[entityType].id);
89-
return (await getEntityProperty(entityType, entityId, PROJECT_IDS)) ?? [];
89+
const stored = await getEntityProperty(entityType, entityId, PROJECT_IDS);
90+
91+
if (!stored) {
92+
return [];
93+
}
94+
95+
if (typeof stored === "string") {
96+
return [stored]; // Wrap single string in array
97+
}
98+
99+
if (Array.isArray(stored)) {
100+
return stored; // Already an array, perfect
101+
}
102+
103+
// Optional: runtime safeguard
104+
throw new Error(`Unexpected type for projectIds: ${typeof stored}`);
90105
};
91106

92107
/** Write Flagsmith Project ID stored on Jira Project */
@@ -96,7 +111,7 @@ export const writeProjectIds = async (
96111
): Promise<void> => {
97112
const entityType = "project";
98113
const entityId = String(extension[entityType].id);
99-
if (projectIds) {
114+
if (projectIds && projectIds.length > 0) {
100115
return await setEntityProperty(entityType, entityId, PROJECT_IDS, projectIds);
101116
} else {
102117
return await deleteEntityProperty(entityType, entityId, PROJECT_IDS);

0 commit comments

Comments
 (0)