Skip to content

Enhance Request Feature #3142

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 16 commits into from
Jun 21, 2025
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions apps/db/supabase/migrations/20250620133223_new_migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
create type "public"."FEATURE_FLAG" as enum ('ENABLE_AI', 'ENABLE_EDUCATION', 'ENABLE_CHALLENGES', 'ENABLE_QUIZZES');

drop index if exists "public"."workspace_education_access_requests_unique_pending";

alter table "public"."workspace_education_access_requests" add column "feature" "FEATURE_FLAG" not null default 'ENABLE_EDUCATION'::"FEATURE_FLAG";

CREATE UNIQUE INDEX workspace_education_access_requests_unique_pending ON public.workspace_education_access_requests USING btree (ws_id, feature) WHERE (status = 'pending'::text);


Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import { Plus, UserPlus } from '@tuturuuu/ui/icons';
import { Separator } from '@tuturuuu/ui/separator';
import { ROOT_WORKSPACE_ID } from '@tuturuuu/utils/constants';
import { getFeatureFlags } from '@tuturuuu/utils/feature-flags/core';

Check warning on line 11 in apps/upskii/src/app/[locale]/(dashboard)/[wsId]/(workspace-settings)/settings/page.tsx

View check run for this annotation

Codecov / codecov/patch

apps/upskii/src/app/[locale]/(dashboard)/[wsId]/(workspace-settings)/settings/page.tsx#L11

Added line #L11 was not covered by tests
import {
getPermissions,
getSecrets,
Expand Down Expand Up @@ -35,6 +36,12 @@
const secrets = await getSecrets({ wsId });
const disableInvite = await verifyHasSecrets(wsId, ['DISABLE_INVITE']);

// Get feature flags for the dialog
const featureFlags = await getFeatureFlags(wsId, true);

// Debug logging
console.log('Server - Feature flags for wsId:', wsId, featureFlags);

Check warning on line 44 in apps/upskii/src/app/[locale]/(dashboard)/[wsId]/(workspace-settings)/settings/page.tsx

View check run for this annotation

Codecov / codecov/patch

apps/upskii/src/app/[locale]/(dashboard)/[wsId]/(workspace-settings)/settings/page.tsx#L39-L44

Added lines #L39 - L44 were not covered by tests
const preventWorkspaceDeletion =
secrets
.find((s) => s.name === 'PREVENT_WORKSPACE_DELETION')
Expand Down Expand Up @@ -105,7 +112,16 @@
title={t('ws-settings.features')}
description={t('ws-settings.features_description')}
action={
<RequestFeatureAccessDialog wsId={wsId} workspaceName={ws?.name}>
<RequestFeatureAccessDialog
wsId={wsId}
workspaceName={ws?.name}
enabledFeatures={{
ENABLE_AI: featureFlags.ENABLE_AI,
ENABLE_EDUCATION: featureFlags.ENABLE_EDUCATION,
ENABLE_QUIZZES: featureFlags.ENABLE_QUIZZES,
ENABLE_CHALLENGES: featureFlags.ENABLE_CHALLENGES,
}}
>

Check warning on line 124 in apps/upskii/src/app/[locale]/(dashboard)/[wsId]/(workspace-settings)/settings/page.tsx

View check run for this annotation

Codecov / codecov/patch

apps/upskii/src/app/[locale]/(dashboard)/[wsId]/(workspace-settings)/settings/page.tsx#L115-L124

Added lines #L115 - L124 were not covered by tests
<Button variant="default" size="default">
<Plus className="mr-2 h-4 w-4" />
Request Features
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { createClient } from '@tuturuuu/supabase/next/server';
import { FEATURE_FLAGS } from '@tuturuuu/utils/feature-flags/data';

Check warning on line 2 in apps/upskii/src/app/api/v1/workspaces/[wsId]/education-access-request/route.ts

View check run for this annotation

Codecov / codecov/patch

apps/upskii/src/app/api/v1/workspaces/[wsId]/education-access-request/route.ts#L2

Added line #L2 was not covered by tests
import { NextRequest, NextResponse } from 'next/server';

interface Params {
Expand All @@ -7,6 +8,27 @@
}>;
}

const FEATURE_MAP = {
ai: {
flag: FEATURE_FLAGS.ENABLE_AI,
name: 'AI',
},
education: {
flag: FEATURE_FLAGS.ENABLE_EDUCATION,
name: 'Education',
},
challenges: {
flag: FEATURE_FLAGS.ENABLE_CHALLENGES,
name: 'Challenges',
},
quizzes: {
flag: FEATURE_FLAGS.ENABLE_QUIZZES,
name: 'Quizzes',
},
} as const;

type FeatureType = keyof typeof FEATURE_MAP;

Check warning on line 31 in apps/upskii/src/app/api/v1/workspaces/[wsId]/education-access-request/route.ts

View check run for this annotation

Codecov / codecov/patch

apps/upskii/src/app/api/v1/workspaces/[wsId]/education-access-request/route.ts#L11-L31

Added lines #L11 - L31 were not covered by tests
export async function POST(req: NextRequest, { params }: Params) {
try {
const supabase = await createClient();
Expand All @@ -24,16 +46,26 @@

// Parse request body
const body = await req.json();
const { workspaceName, message } = body;
const { workspaceName, message, feature } = body;

Check warning on line 49 in apps/upskii/src/app/api/v1/workspaces/[wsId]/education-access-request/route.ts

View check run for this annotation

Codecov / codecov/patch

apps/upskii/src/app/api/v1/workspaces/[wsId]/education-access-request/route.ts#L49

Added line #L49 was not covered by tests

// Validate required fields
if (!workspaceName?.trim() || !message?.trim()) {
if (!workspaceName?.trim() || !message?.trim() || !feature) {

Check warning on line 52 in apps/upskii/src/app/api/v1/workspaces/[wsId]/education-access-request/route.ts

View check run for this annotation

Codecov / codecov/patch

apps/upskii/src/app/api/v1/workspaces/[wsId]/education-access-request/route.ts#L52

Added line #L52 was not covered by tests
return NextResponse.json(
{ error: 'Workspace name and message are required' },
{ error: 'Workspace name, message, and feature are required' },

Check warning on line 54 in apps/upskii/src/app/api/v1/workspaces/[wsId]/education-access-request/route.ts

View check run for this annotation

Codecov / codecov/patch

apps/upskii/src/app/api/v1/workspaces/[wsId]/education-access-request/route.ts#L54

Added line #L54 was not covered by tests
{ status: 400 }
);
}

// Validate feature type
if (!FEATURE_MAP[feature as FeatureType]) {
return NextResponse.json(
{ error: 'Invalid feature type' },
{ status: 400 }
);
}

const featureConfig = FEATURE_MAP[feature as FeatureType];

Check warning on line 68 in apps/upskii/src/app/api/v1/workspaces/[wsId]/education-access-request/route.ts

View check run for this annotation

Codecov / codecov/patch

apps/upskii/src/app/api/v1/workspaces/[wsId]/education-access-request/route.ts#L59-L68

Added lines #L59 - L68 were not covered by tests
// Verify user is workspace owner
const { data: memberCheck, error: memberError } = await supabase
.from('workspace_members')
Expand All @@ -51,52 +83,57 @@

if (memberCheck.role !== 'OWNER') {
return NextResponse.json(
{ error: 'Only workspace owners can request education access' },
{
error: `Only workspace owners can request ${featureConfig.name} access`,
},

Check warning on line 88 in apps/upskii/src/app/api/v1/workspaces/[wsId]/education-access-request/route.ts

View check run for this annotation

Codecov / codecov/patch

apps/upskii/src/app/api/v1/workspaces/[wsId]/education-access-request/route.ts#L86-L88

Added lines #L86 - L88 were not covered by tests
{ status: 403 }
);
}

// Check if education is already enabled
const { data: educationSecret } = await supabase
// Check if feature is already enabled
const { data: featureSecret } = await supabase

Check warning on line 94 in apps/upskii/src/app/api/v1/workspaces/[wsId]/education-access-request/route.ts

View check run for this annotation

Codecov / codecov/patch

apps/upskii/src/app/api/v1/workspaces/[wsId]/education-access-request/route.ts#L93-L94

Added lines #L93 - L94 were not covered by tests
.from('workspace_secrets')
.select('value')
.eq('ws_id', wsId)
.eq('name', 'ENABLE_EDUCATION')
.eq('name', featureConfig.flag)

Check warning on line 98 in apps/upskii/src/app/api/v1/workspaces/[wsId]/education-access-request/route.ts

View check run for this annotation

Codecov / codecov/patch

apps/upskii/src/app/api/v1/workspaces/[wsId]/education-access-request/route.ts#L98

Added line #L98 was not covered by tests
.single();

if (educationSecret?.value === 'true') {
if (featureSecret?.value === 'true') {

Check warning on line 101 in apps/upskii/src/app/api/v1/workspaces/[wsId]/education-access-request/route.ts

View check run for this annotation

Codecov / codecov/patch

apps/upskii/src/app/api/v1/workspaces/[wsId]/education-access-request/route.ts#L101

Added line #L101 was not covered by tests
return NextResponse.json(
{ error: 'Education features are already enabled for this workspace' },
{
error: `${featureConfig.name} features are already enabled for this workspace`,
},

Check warning on line 105 in apps/upskii/src/app/api/v1/workspaces/[wsId]/education-access-request/route.ts

View check run for this annotation

Codecov / codecov/patch

apps/upskii/src/app/api/v1/workspaces/[wsId]/education-access-request/route.ts#L103-L105

Added lines #L103 - L105 were not covered by tests
{ status: 400 }
);
}

// Check for existing pending request
// Check for existing pending request for this feature

Check warning on line 110 in apps/upskii/src/app/api/v1/workspaces/[wsId]/education-access-request/route.ts

View check run for this annotation

Codecov / codecov/patch

apps/upskii/src/app/api/v1/workspaces/[wsId]/education-access-request/route.ts#L110

Added line #L110 was not covered by tests
const { data: existingRequest, error: existingError } = await supabase
.from('workspace_education_access_requests')
.select('id, status')
.eq('ws_id', wsId)
.eq('feature', featureConfig.flag)

Check warning on line 115 in apps/upskii/src/app/api/v1/workspaces/[wsId]/education-access-request/route.ts

View check run for this annotation

Codecov / codecov/patch

apps/upskii/src/app/api/v1/workspaces/[wsId]/education-access-request/route.ts#L115

Added line #L115 was not covered by tests
.eq('status', 'pending')
.single();

if (existingRequest && !existingError) {
return NextResponse.json(
{
error:
'A pending education access request already exists for this workspace',
error: `A pending ${featureConfig.name} access request already exists for this workspace`,

Check warning on line 122 in apps/upskii/src/app/api/v1/workspaces/[wsId]/education-access-request/route.ts

View check run for this annotation

Codecov / codecov/patch

apps/upskii/src/app/api/v1/workspaces/[wsId]/education-access-request/route.ts#L122

Added line #L122 was not covered by tests
},
{ status: 400 }
);
}

// Create the education access request
// Create the feature access request

Check warning on line 128 in apps/upskii/src/app/api/v1/workspaces/[wsId]/education-access-request/route.ts

View check run for this annotation

Codecov / codecov/patch

apps/upskii/src/app/api/v1/workspaces/[wsId]/education-access-request/route.ts#L128

Added line #L128 was not covered by tests
const { data: newRequest, error: insertError } = await supabase
.from('workspace_education_access_requests')
.insert({
ws_id: wsId,
workspace_name: workspaceName.trim(),
creator_id: user.id,
message: message.trim(),
feature: featureConfig.flag,

Check warning on line 136 in apps/upskii/src/app/api/v1/workspaces/[wsId]/education-access-request/route.ts

View check run for this annotation

Codecov / codecov/patch

apps/upskii/src/app/api/v1/workspaces/[wsId]/education-access-request/route.ts#L136

Added line #L136 was not covered by tests
status: 'pending',
})
.select('*')
Expand All @@ -105,14 +142,14 @@
if (insertError) {
console.error('Database error:', insertError);
return NextResponse.json(
{ error: 'Failed to create education access request' },
{ error: `Failed to create ${featureConfig.name} access request` },

Check warning on line 145 in apps/upskii/src/app/api/v1/workspaces/[wsId]/education-access-request/route.ts

View check run for this annotation

Codecov / codecov/patch

apps/upskii/src/app/api/v1/workspaces/[wsId]/education-access-request/route.ts#L145

Added line #L145 was not covered by tests
{ status: 500 }
);
}

return NextResponse.json(
{
message: 'Education access request submitted successfully',
message: `${featureConfig.name} access request submitted successfully`,

Check warning on line 152 in apps/upskii/src/app/api/v1/workspaces/[wsId]/education-access-request/route.ts

View check run for this annotation

Codecov / codecov/patch

apps/upskii/src/app/api/v1/workspaces/[wsId]/education-access-request/route.ts#L152

Added line #L152 was not covered by tests
request: newRequest,
},
{ status: 201 }
Expand Down Expand Up @@ -156,29 +193,22 @@
);
}

// Get education access request for this workspace
const { data: request, error: requestError } = await supabase
// Get all feature access requests for this workspace
const { data: requests, error: requestError } = await supabase

Check warning on line 197 in apps/upskii/src/app/api/v1/workspaces/[wsId]/education-access-request/route.ts

View check run for this annotation

Codecov / codecov/patch

apps/upskii/src/app/api/v1/workspaces/[wsId]/education-access-request/route.ts#L196-L197

Added lines #L196 - L197 were not covered by tests
.from('workspace_education_access_requests')
.select('*')
.eq('ws_id', wsId)
.order('created_at', { ascending: false })
.limit(1)
.single();
.order('created_at', { ascending: false });

Check warning on line 201 in apps/upskii/src/app/api/v1/workspaces/[wsId]/education-access-request/route.ts

View check run for this annotation

Codecov / codecov/patch

apps/upskii/src/app/api/v1/workspaces/[wsId]/education-access-request/route.ts#L201

Added line #L201 was not covered by tests

if (requestError) {
// If no request found, return null
if (requestError.code === 'PGRST116') {
return NextResponse.json({ request: null });
}

console.error('Database error:', requestError);
return NextResponse.json(
{ error: 'Failed to fetch education access request' },
{ error: 'Failed to fetch feature access requests' },

Check warning on line 206 in apps/upskii/src/app/api/v1/workspaces/[wsId]/education-access-request/route.ts

View check run for this annotation

Codecov / codecov/patch

apps/upskii/src/app/api/v1/workspaces/[wsId]/education-access-request/route.ts#L206

Added line #L206 was not covered by tests
{ status: 500 }
);
}

return NextResponse.json({ request });
return NextResponse.json({ requests: requests || [] });

Check warning on line 211 in apps/upskii/src/app/api/v1/workspaces/[wsId]/education-access-request/route.ts

View check run for this annotation

Codecov / codecov/patch

apps/upskii/src/app/api/v1/workspaces/[wsId]/education-access-request/route.ts#L211

Added line #L211 was not covered by tests
} catch (error) {
console.error('Unexpected error:', error);
return NextResponse.json(
Expand Down
43 changes: 39 additions & 4 deletions apps/upskii/src/components/request-feature-access-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@
workspaceName: string | null;
wsId: string;
children: React.ReactNode;
enabledFeatures?: {
ENABLE_AI: boolean;
ENABLE_EDUCATION: boolean;
ENABLE_QUIZZES: boolean;
ENABLE_CHALLENGES: boolean;
};
}

interface FeatureAccessRequest {
Expand Down Expand Up @@ -99,6 +105,12 @@
workspaceName,
wsId,
children,
enabledFeatures = {
ENABLE_AI: false,
ENABLE_EDUCATION: false,
ENABLE_QUIZZES: false,
ENABLE_CHALLENGES: false,
},

Check warning on line 113 in apps/upskii/src/components/request-feature-access-dialog.tsx

View check run for this annotation

Codecov / codecov/patch

apps/upskii/src/components/request-feature-access-dialog.tsx#L108-L113

Added lines #L108 - L113 were not covered by tests
}: RequestFeatureAccessDialogProps) {
const t = useTranslations('ws-settings.feature-request');

Expand All @@ -119,10 +131,32 @@
.filter((r) => r.status === 'pending' || r.status === 'approved')
.map((r) => r.feature)
);
return Object.keys(featureConfig).filter(
(f) => !requestedOrApprovedFeatures.has(f)

// Also exclude features that are already enabled
const enabledFeaturesSet = new Set();
if (enabledFeatures?.ENABLE_AI) enabledFeaturesSet.add('ai');
if (enabledFeatures?.ENABLE_EDUCATION) enabledFeaturesSet.add('education');
if (enabledFeatures?.ENABLE_QUIZZES) enabledFeaturesSet.add('quizzes');
if (enabledFeatures?.ENABLE_CHALLENGES)
enabledFeaturesSet.add('challenges');

Check warning on line 141 in apps/upskii/src/components/request-feature-access-dialog.tsx

View check run for this annotation

Codecov / codecov/patch

apps/upskii/src/components/request-feature-access-dialog.tsx#L136-L141

Added lines #L136 - L141 were not covered by tests

// Debug logging
console.log('Debug - enabledFeatures:', enabledFeatures);
console.log('Debug - enabledFeaturesSet:', enabledFeaturesSet);
console.log(
'Debug - requestedOrApprovedFeatures:',
requestedOrApprovedFeatures
);
console.log('Debug - all feature keys:', Object.keys(featureConfig));

Check warning on line 150 in apps/upskii/src/components/request-feature-access-dialog.tsx

View check run for this annotation

Codecov / codecov/patch

apps/upskii/src/components/request-feature-access-dialog.tsx#L144-L150

Added lines #L144 - L150 were not covered by tests

const available = Object.keys(featureConfig).filter(
(f) => !requestedOrApprovedFeatures.has(f) && !enabledFeaturesSet.has(f)

Check warning on line 153 in apps/upskii/src/components/request-feature-access-dialog.tsx

View check run for this annotation

Codecov / codecov/patch

apps/upskii/src/components/request-feature-access-dialog.tsx#L152-L153

Added lines #L152 - L153 were not covered by tests
) as FeatureKey[];
}, [existingRequests]);

console.log('Debug - availableFeatures:', available);

Check warning on line 156 in apps/upskii/src/components/request-feature-access-dialog.tsx

View check run for this annotation

Codecov / codecov/patch

apps/upskii/src/components/request-feature-access-dialog.tsx#L156

Added line #L156 was not covered by tests

return available;
}, [existingRequests, enabledFeatures]);

Check warning on line 159 in apps/upskii/src/components/request-feature-access-dialog.tsx

View check run for this annotation

Codecov / codecov/patch

apps/upskii/src/components/request-feature-access-dialog.tsx#L158-L159

Added lines #L158 - L159 were not covered by tests

useEffect(() => {
const firstFeature = availableFeatures[0];
Expand Down Expand Up @@ -174,7 +208,7 @@
setIsLoading(true);
try {
const response = await fetch(
`/api/v1/workspaces/${wsId}/feature-access-requests`,
`/api/v1/workspaces/${wsId}/education-access-request`,

Check warning on line 211 in apps/upskii/src/components/request-feature-access-dialog.tsx

View check run for this annotation

Codecov / codecov/patch

apps/upskii/src/components/request-feature-access-dialog.tsx#L211

Added line #L211 was not covered by tests
{
method: 'POST',
headers: {
Expand All @@ -195,6 +229,7 @@
await checkExistingRequests(); // Refresh requests list
setOpen(false);
setMessage('');
setSelectedFeature(null);

Check warning on line 232 in apps/upskii/src/components/request-feature-access-dialog.tsx

View check run for this annotation

Codecov / codecov/patch

apps/upskii/src/components/request-feature-access-dialog.tsx#L232

Added line #L232 was not covered by tests
} else {
toast.error(data.error || t('toasts.error.request-failed'));
}
Expand Down
Loading