implemented organisation features

This commit is contained in:
2026-01-24 19:44:40 +00:00
parent c37c3742b9
commit f65ad0c593
8 changed files with 120 additions and 106 deletions

View File

@@ -5,7 +5,7 @@
"identifier": "com.hex248.sprint",
"build": {
"beforeDevCommand": "bun run dev",
"devUrl": "http://localhost:1420/app",
"devUrl": "http://localhost:1420/issues",
"beforeBuildCommand": "bun run build",
"frontendDist": "../dist"
},
@@ -18,7 +18,7 @@
"minWidth": 640,
"minHeight": 360,
"decorations": false,
"url": "/app"
"url": "/issues"
}
],
"security": {

View File

@@ -1,5 +1,6 @@
import { useSession } from "@/components/session-provider";
import Icon from "@/components/ui/icon";
import { useSelectedOrganisation } from "@/lib/query/hooks";
import { cn } from "@/lib/utils";
const FALLBACK_COLOURS = [
@@ -59,6 +60,7 @@ export default function Avatar({
className?: string;
}) {
// if the username matches the authed user, use their avatarURL and name (avoid stale data)
const selectedOrganisation = useSelectedOrganisation();
const { user } = useSession();
const avatarURL = !strong && username && user && username === user.username ? user.avatarURL : _avatarURL;
const name = !strong && username && user && username === user.username ? user.name : _name;
@@ -73,14 +75,15 @@ export default function Avatar({
"flex items-center justify-center rounded-full",
"text-white font-medium select-none",
name && "border",
!avatarURL && backgroundClass,
(!avatarURL || !selectedOrganisation?.Organisation.features.userAvatars) && backgroundClass,
"transition-colors",
`w-${size || 6}`,
`h-${size || 6}`,
className,
)}
>
{avatarURL ? (
{selectedOrganisation?.Organisation.features.userAvatars && avatarURL ? (
<img
src={avatarURL}
alt="Avatar"

View File

@@ -18,7 +18,7 @@ import { IconButton } from "@/components/ui/icon-button";
import { Input } from "@/components/ui/input";
import { SelectTrigger } from "@/components/ui/select";
import { Textarea } from "@/components/ui/textarea";
import { useDeleteIssue, useUpdateIssue } from "@/lib/query/hooks";
import { useDeleteIssue, useSelectedOrganisation, useUpdateIssue } from "@/lib/query/hooks";
import { parseError } from "@/lib/server";
import { cn, issueID } from "@/lib/utils";
@@ -51,6 +51,7 @@ export function IssueDetails({
showHeader?: boolean;
}) {
const { user } = useSession();
const organisation = useSelectedOrganisation();
const updateIssue = useUpdateIssue();
const deleteIssue = useDeleteIssue();
@@ -320,6 +321,7 @@ export function IssueDetails({
<div className="flex flex-col w-full p-2 py-2 gap-2 max-h-[75vh] overflow-y-scroll">
<div className="flex gap-2">
{organisation?.Organisation.features.issueStatus && (
<StatusSelect
statuses={statuses}
value={status}
@@ -335,6 +337,7 @@ export function IssueDetails({
</SelectTrigger>
)}
/>
)}
<div className="flex w-full items-center min-w-0">
<Input
value={title}
@@ -357,7 +360,8 @@ export function IssueDetails({
/>
</div>
</div>
{description || isEditingDescription ? (
{organisation?.Organisation.features.description &&
(description || isEditingDescription ? (
<Textarea
ref={descriptionRef}
value={description}
@@ -388,12 +392,16 @@ export function IssueDetails({
>
Add description
</Button>
)}
))}
{organisation?.Organisation.features.sprints && (
<div className="flex items-center gap-2">
<span className="text-sm">Sprint:</span>
<SprintSelect sprints={sprints} value={sprintId} onChange={handleSprintChange} />
</div>
)}
{organisation?.Organisation.features.issueAssignees && (
<div className="flex items-start gap-2">
<span className="text-sm pt-2">Assignees:</span>
<MultiAssigneeSelect
@@ -403,13 +411,16 @@ export function IssueDetails({
fallbackUsers={issueData.Assignees}
/>
</div>
)}
{organisation?.Organisation.features.issueCreator && (
<div className="flex items-center gap-2">
<span className="text-sm">Created by:</span>
<SmallUserDisplay user={issueData.Creator} className={"text-sm"} />
</div>
)}
{isAssignee && (
{organisation?.Organisation.features.issueTimeTracking && isAssignee && (
<div className={cn("flex flex-col gap-2", hasMultipleAssignees && "cursor-not-allowed")}>
<div className="flex items-center gap-2">
<TimerModal issueId={issueData.Issue.id} disabled={hasMultipleAssignees} />
@@ -423,7 +434,9 @@ export function IssueDetails({
</div>
)}
{organisation?.Organisation.features.issueComments && (
<IssueComments issueId={issueData.Issue.id} className="pt-2" />
)}
<ConfirmDialog
open={deleteOpen}

View File

@@ -90,7 +90,8 @@ export function IssuesTable({
onClick={handleLinkClick}
className="flex items-center gap-2 min-w-0 w-full h-full px-2 py-1 text-inherit hover:underline decoration-transparent"
>
{(columns.status == null || columns.status === true) && (
{selectedOrganisation?.Organisation.features.issueStatus &&
(columns.status == null || columns.status === true) && (
<StatusTag status={issueData.Issue.status} colour={statuses[issueData.Issue.status]} />
)}
<span className="truncate">{issueData.Issue.title}</span>
@@ -115,7 +116,9 @@ export function IssuesTable({
onClick={handleLinkClick}
className="flex items-center justify-end w-full h-full px-2"
>
{issueData.Assignees && issueData.Assignees.length > 0 && (
{selectedOrganisation?.Organisation.features.issueAssigneesShownInTable &&
issueData.Assignees &&
issueData.Assignees.length > 0 && (
<div className="flex items-center -space-x-2 pr-1.5">
{issueData.Assignees.slice(0, 3).map((assignee) => (
<Avatar

View File

@@ -929,8 +929,7 @@ function Organisations({ trigger }: { trigger?: ReactNode }) {
<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)}:{" "}
<div key={feature} className="flex items-center gap-2 p-1">
<Switch
checked={Boolean(selectedOrganisation?.Organisation.features[feature])}
onCheckedChange={async (checked) => {
@@ -949,7 +948,9 @@ function Organisations({ trigger }: { trigger?: ReactNode }) {
);
await invalidateOrganisations();
}}
color={"#ff0000"}
/>
<span className={"text-sm"}>{unCamelCase(feature)}</span>
</div>
))}
</div>

View File

@@ -1,4 +1,4 @@
import { useMemo } from "react";
import { useEffect, useMemo } from "react";
import { useLocation, useNavigate } from "react-router-dom";
import Account from "@/components/account";
import { IssueForm } from "@/components/issue-form";
@@ -38,6 +38,11 @@ export default function TopBar({ showIssueForm = true }: { showIssueForm?: boole
[organisationsData, selectedOrganisationId],
);
useEffect(() => {
if (selectedOrganisation?.Organisation.features.sprints === false && activeView === "timeline") {
navigate("/issues");
}
}, [selectedOrganisation, activeView, navigate]);
return (
<div className="flex gap-12 items-center justify-between">
<div className={`flex gap-${BREATHING_ROOM} items-center`}>
@@ -55,7 +60,7 @@ export default function TopBar({ showIssueForm = true }: { showIssueForm?: boole
/>
{selectedOrganisationId && <ProjectSelect showLabel />}
{selectedOrganisationId && (
{selectedOrganisation?.Organisation.features.sprints && selectedOrganisationId && (
<Tabs
value={activeView}
onValueChange={(value) => {

View File

@@ -29,14 +29,6 @@ ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
<Route path="/login" element={<Login />} />
{/* authed routes */}
<Route
path="/app"
element={
<RequireAuth>
<Navigate to="/issues" replace />
</RequireAuth>
}
/>
<Route
path="/issues"
element={

View File

@@ -12,12 +12,9 @@
# LOW PRIORITY
- disable self-hosting stuff
- make closed source
- dedicated /register route (currently login/register are combined on /login)
- real logo
- org settings
- disable individual features
- manage issue types, default is [bug, feature]
- create, edit, delete
- assign icons to issue types (ensure each available icon is in EACH icon set)