toggle features per organisation

This commit is contained in:
2026-01-24 14:49:18 +00:00
parent 9d8aee7a74
commit c37c3742b9
3 changed files with 46 additions and 4 deletions

View File

@@ -1,4 +1,9 @@
import { DEFAULT_STATUS_COLOUR, ISSUE_STATUS_MAX_LENGTH, type SprintRecord } from "@sprint/shared"; import {
DEFAULT_FEATURES,
DEFAULT_STATUS_COLOUR,
ISSUE_STATUS_MAX_LENGTH,
type SprintRecord,
} from "@sprint/shared";
import { useQueryClient } from "@tanstack/react-query"; import { useQueryClient } from "@tanstack/react-query";
import { type ReactNode, useCallback, useEffect, useMemo, useState } from "react"; import { type ReactNode, useCallback, useEffect, useMemo, useState } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
@@ -44,7 +49,8 @@ import {
} from "@/lib/query/hooks"; } from "@/lib/query/hooks";
import { queryKeys } from "@/lib/query/keys"; import { queryKeys } from "@/lib/query/keys";
import { issue } from "@/lib/server"; import { issue } from "@/lib/server";
import { capitalise } from "@/lib/utils"; import { capitalise, unCamelCase } from "@/lib/utils";
import { Switch } from "./ui/switch";
function Organisations({ trigger }: { trigger?: ReactNode }) { function Organisations({ trigger }: { trigger?: ReactNode }) {
const { user } = useAuthenticatedSession(); const { user } = useAuthenticatedSession();
@@ -440,7 +446,7 @@ function Organisations({ trigger }: { trigger?: ReactNode }) {
)} )}
</DialogTrigger> </DialogTrigger>
<DialogContent className="max-w-sm"> <DialogContent className="w-md max-w-md">
<DialogHeader> <DialogHeader>
<DialogTitle>Organisations</DialogTitle> <DialogTitle>Organisations</DialogTitle>
</DialogHeader> </DialogHeader>
@@ -457,6 +463,7 @@ function Organisations({ trigger }: { trigger?: ReactNode }) {
<TabsTrigger value="users">Users</TabsTrigger> <TabsTrigger value="users">Users</TabsTrigger>
<TabsTrigger value="projects">Projects</TabsTrigger> <TabsTrigger value="projects">Projects</TabsTrigger>
<TabsTrigger value="issues">Issues</TabsTrigger> <TabsTrigger value="issues">Issues</TabsTrigger>
<TabsTrigger value="features">Features</TabsTrigger>
</TabsList> </TabsList>
</div> </div>
@@ -917,6 +924,37 @@ function Organisations({ trigger }: { trigger?: ReactNode }) {
</div> </div>
</div> </div>
</TabsContent> </TabsContent>
<TabsContent value="features">
<div className="border p-2 min-w-0 overflow-hidden">
<h2 className="text-xl font-600 mb-2">Features</h2>
<div className="flex flex-col gap-2 w-full">
{Object.keys(DEFAULT_FEATURES).map((feature) => (
<div key={feature}>
{unCamelCase(feature)}:{" "}
<Switch
checked={Boolean(selectedOrganisation?.Organisation.features[feature])}
onCheckedChange={async (checked) => {
if (!selectedOrganisation) return;
const newFeatures = selectedOrganisation.Organisation.features;
newFeatures[feature] = checked;
await updateOrganisation.mutateAsync({
id: selectedOrganisation.Organisation.id,
features: newFeatures,
});
toast.success(
`${capitalise(unCamelCase(feature))} ${
checked ? "enabled" : "disabled"
} for ${selectedOrganisation.Organisation.name}`,
);
await invalidateOrganisations();
}}
/>
</div>
))}
</div>
</div>
</TabsContent>
</Tabs> </Tabs>
) : ( ) : (
<div className="flex flex-col gap-2 w-full min-w-0"> <div className="flex flex-col gap-2 w-full min-w-0">

View File

@@ -208,7 +208,7 @@ export function SprintForm({
isEdit && existingSprint ? sprints.filter((s) => s.id !== existingSprint.id) : sprints; isEdit && existingSprint ? sprints.filter((s) => s.id !== existingSprint.id) : sprints;
const dialogContent = ( const dialogContent = (
<DialogContent className={cn("w-md", (error || dateError) && "border-destructive")}> <DialogContent className={cn("w-sm", (error || dateError) && "border-destructive")}>
<DialogHeader> <DialogHeader>
<DialogTitle>{isEdit ? "Edit Sprint" : "Create Sprint"}</DialogTitle> <DialogTitle>{isEdit ? "Edit Sprint" : "Create Sprint"}</DialogTitle>
</DialogHeader> </DialogHeader>

View File

@@ -65,3 +65,7 @@ export const isLight = (hex: string): boolean => {
const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255; const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
return luminance > THRESHOLD; return luminance > THRESHOLD;
}; };
export const unCamelCase = (str: string): string => {
return str.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/^./, (char) => char.toUpperCase());
};