mirror of
https://github.com/hex248/sprint.git
synced 2026-02-08 02:33:01 +00:00
implemented organisation features
This commit is contained in:
@@ -5,7 +5,7 @@
|
|||||||
"identifier": "com.hex248.sprint",
|
"identifier": "com.hex248.sprint",
|
||||||
"build": {
|
"build": {
|
||||||
"beforeDevCommand": "bun run dev",
|
"beforeDevCommand": "bun run dev",
|
||||||
"devUrl": "http://localhost:1420/app",
|
"devUrl": "http://localhost:1420/issues",
|
||||||
"beforeBuildCommand": "bun run build",
|
"beforeBuildCommand": "bun run build",
|
||||||
"frontendDist": "../dist"
|
"frontendDist": "../dist"
|
||||||
},
|
},
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
"minWidth": 640,
|
"minWidth": 640,
|
||||||
"minHeight": 360,
|
"minHeight": 360,
|
||||||
"decorations": false,
|
"decorations": false,
|
||||||
"url": "/app"
|
"url": "/issues"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"security": {
|
"security": {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useSession } from "@/components/session-provider";
|
import { useSession } from "@/components/session-provider";
|
||||||
import Icon from "@/components/ui/icon";
|
import Icon from "@/components/ui/icon";
|
||||||
|
import { useSelectedOrganisation } from "@/lib/query/hooks";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const FALLBACK_COLOURS = [
|
const FALLBACK_COLOURS = [
|
||||||
@@ -59,6 +60,7 @@ export default function Avatar({
|
|||||||
className?: string;
|
className?: string;
|
||||||
}) {
|
}) {
|
||||||
// if the username matches the authed user, use their avatarURL and name (avoid stale data)
|
// if the username matches the authed user, use their avatarURL and name (avoid stale data)
|
||||||
|
const selectedOrganisation = useSelectedOrganisation();
|
||||||
const { user } = useSession();
|
const { user } = useSession();
|
||||||
const avatarURL = !strong && username && user && username === user.username ? user.avatarURL : _avatarURL;
|
const avatarURL = !strong && username && user && username === user.username ? user.avatarURL : _avatarURL;
|
||||||
const name = !strong && username && user && username === user.username ? user.name : _name;
|
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",
|
"flex items-center justify-center rounded-full",
|
||||||
"text-white font-medium select-none",
|
"text-white font-medium select-none",
|
||||||
name && "border",
|
name && "border",
|
||||||
!avatarURL && backgroundClass,
|
(!avatarURL || !selectedOrganisation?.Organisation.features.userAvatars) && backgroundClass,
|
||||||
|
|
||||||
"transition-colors",
|
"transition-colors",
|
||||||
`w-${size || 6}`,
|
`w-${size || 6}`,
|
||||||
`h-${size || 6}`,
|
`h-${size || 6}`,
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{avatarURL ? (
|
{selectedOrganisation?.Organisation.features.userAvatars && avatarURL ? (
|
||||||
<img
|
<img
|
||||||
src={avatarURL}
|
src={avatarURL}
|
||||||
alt="Avatar"
|
alt="Avatar"
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import { IconButton } from "@/components/ui/icon-button";
|
|||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { SelectTrigger } from "@/components/ui/select";
|
import { SelectTrigger } from "@/components/ui/select";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
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 { parseError } from "@/lib/server";
|
||||||
import { cn, issueID } from "@/lib/utils";
|
import { cn, issueID } from "@/lib/utils";
|
||||||
|
|
||||||
@@ -51,6 +51,7 @@ export function IssueDetails({
|
|||||||
showHeader?: boolean;
|
showHeader?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const { user } = useSession();
|
const { user } = useSession();
|
||||||
|
const organisation = useSelectedOrganisation();
|
||||||
const updateIssue = useUpdateIssue();
|
const updateIssue = useUpdateIssue();
|
||||||
const deleteIssue = useDeleteIssue();
|
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 flex-col w-full p-2 py-2 gap-2 max-h-[75vh] overflow-y-scroll">
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
|
{organisation?.Organisation.features.issueStatus && (
|
||||||
<StatusSelect
|
<StatusSelect
|
||||||
statuses={statuses}
|
statuses={statuses}
|
||||||
value={status}
|
value={status}
|
||||||
@@ -335,6 +337,7 @@ export function IssueDetails({
|
|||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
<div className="flex w-full items-center min-w-0">
|
<div className="flex w-full items-center min-w-0">
|
||||||
<Input
|
<Input
|
||||||
value={title}
|
value={title}
|
||||||
@@ -357,7 +360,8 @@ export function IssueDetails({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{description || isEditingDescription ? (
|
{organisation?.Organisation.features.description &&
|
||||||
|
(description || isEditingDescription ? (
|
||||||
<Textarea
|
<Textarea
|
||||||
ref={descriptionRef}
|
ref={descriptionRef}
|
||||||
value={description}
|
value={description}
|
||||||
@@ -388,12 +392,16 @@ export function IssueDetails({
|
|||||||
>
|
>
|
||||||
Add description
|
Add description
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
))}
|
||||||
|
|
||||||
|
{organisation?.Organisation.features.sprints && (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-sm">Sprint:</span>
|
<span className="text-sm">Sprint:</span>
|
||||||
<SprintSelect sprints={sprints} value={sprintId} onChange={handleSprintChange} />
|
<SprintSelect sprints={sprints} value={sprintId} onChange={handleSprintChange} />
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{organisation?.Organisation.features.issueAssignees && (
|
||||||
<div className="flex items-start gap-2">
|
<div className="flex items-start gap-2">
|
||||||
<span className="text-sm pt-2">Assignees:</span>
|
<span className="text-sm pt-2">Assignees:</span>
|
||||||
<MultiAssigneeSelect
|
<MultiAssigneeSelect
|
||||||
@@ -403,13 +411,16 @@ export function IssueDetails({
|
|||||||
fallbackUsers={issueData.Assignees}
|
fallbackUsers={issueData.Assignees}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{organisation?.Organisation.features.issueCreator && (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-sm">Created by:</span>
|
<span className="text-sm">Created by:</span>
|
||||||
<SmallUserDisplay user={issueData.Creator} className={"text-sm"} />
|
<SmallUserDisplay user={issueData.Creator} className={"text-sm"} />
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{isAssignee && (
|
{organisation?.Organisation.features.issueTimeTracking && isAssignee && (
|
||||||
<div className={cn("flex flex-col gap-2", hasMultipleAssignees && "cursor-not-allowed")}>
|
<div className={cn("flex flex-col gap-2", hasMultipleAssignees && "cursor-not-allowed")}>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<TimerModal issueId={issueData.Issue.id} disabled={hasMultipleAssignees} />
|
<TimerModal issueId={issueData.Issue.id} disabled={hasMultipleAssignees} />
|
||||||
@@ -423,7 +434,9 @@ export function IssueDetails({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{organisation?.Organisation.features.issueComments && (
|
||||||
<IssueComments issueId={issueData.Issue.id} className="pt-2" />
|
<IssueComments issueId={issueData.Issue.id} className="pt-2" />
|
||||||
|
)}
|
||||||
|
|
||||||
<ConfirmDialog
|
<ConfirmDialog
|
||||||
open={deleteOpen}
|
open={deleteOpen}
|
||||||
|
|||||||
@@ -90,7 +90,8 @@ export function IssuesTable({
|
|||||||
onClick={handleLinkClick}
|
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"
|
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]} />
|
<StatusTag status={issueData.Issue.status} colour={statuses[issueData.Issue.status]} />
|
||||||
)}
|
)}
|
||||||
<span className="truncate">{issueData.Issue.title}</span>
|
<span className="truncate">{issueData.Issue.title}</span>
|
||||||
@@ -115,7 +116,9 @@ export function IssuesTable({
|
|||||||
onClick={handleLinkClick}
|
onClick={handleLinkClick}
|
||||||
className="flex items-center justify-end w-full h-full px-2"
|
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">
|
<div className="flex items-center -space-x-2 pr-1.5">
|
||||||
{issueData.Assignees.slice(0, 3).map((assignee) => (
|
{issueData.Assignees.slice(0, 3).map((assignee) => (
|
||||||
<Avatar
|
<Avatar
|
||||||
|
|||||||
@@ -929,8 +929,7 @@ function Organisations({ trigger }: { trigger?: ReactNode }) {
|
|||||||
<h2 className="text-xl font-600 mb-2">Features</h2>
|
<h2 className="text-xl font-600 mb-2">Features</h2>
|
||||||
<div className="flex flex-col gap-2 w-full">
|
<div className="flex flex-col gap-2 w-full">
|
||||||
{Object.keys(DEFAULT_FEATURES).map((feature) => (
|
{Object.keys(DEFAULT_FEATURES).map((feature) => (
|
||||||
<div key={feature}>
|
<div key={feature} className="flex items-center gap-2 p-1">
|
||||||
{unCamelCase(feature)}:{" "}
|
|
||||||
<Switch
|
<Switch
|
||||||
checked={Boolean(selectedOrganisation?.Organisation.features[feature])}
|
checked={Boolean(selectedOrganisation?.Organisation.features[feature])}
|
||||||
onCheckedChange={async (checked) => {
|
onCheckedChange={async (checked) => {
|
||||||
@@ -949,7 +948,9 @@ function Organisations({ trigger }: { trigger?: ReactNode }) {
|
|||||||
);
|
);
|
||||||
await invalidateOrganisations();
|
await invalidateOrganisations();
|
||||||
}}
|
}}
|
||||||
|
color={"#ff0000"}
|
||||||
/>
|
/>
|
||||||
|
<span className={"text-sm"}>{unCamelCase(feature)}</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useMemo } from "react";
|
import { useEffect, useMemo } from "react";
|
||||||
import { useLocation, useNavigate } from "react-router-dom";
|
import { useLocation, useNavigate } from "react-router-dom";
|
||||||
import Account from "@/components/account";
|
import Account from "@/components/account";
|
||||||
import { IssueForm } from "@/components/issue-form";
|
import { IssueForm } from "@/components/issue-form";
|
||||||
@@ -38,6 +38,11 @@ export default function TopBar({ showIssueForm = true }: { showIssueForm?: boole
|
|||||||
[organisationsData, selectedOrganisationId],
|
[organisationsData, selectedOrganisationId],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedOrganisation?.Organisation.features.sprints === false && activeView === "timeline") {
|
||||||
|
navigate("/issues");
|
||||||
|
}
|
||||||
|
}, [selectedOrganisation, activeView, navigate]);
|
||||||
return (
|
return (
|
||||||
<div className="flex gap-12 items-center justify-between">
|
<div className="flex gap-12 items-center justify-between">
|
||||||
<div className={`flex gap-${BREATHING_ROOM} items-center`}>
|
<div className={`flex gap-${BREATHING_ROOM} items-center`}>
|
||||||
@@ -55,7 +60,7 @@ export default function TopBar({ showIssueForm = true }: { showIssueForm?: boole
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{selectedOrganisationId && <ProjectSelect showLabel />}
|
{selectedOrganisationId && <ProjectSelect showLabel />}
|
||||||
{selectedOrganisationId && (
|
{selectedOrganisation?.Organisation.features.sprints && selectedOrganisationId && (
|
||||||
<Tabs
|
<Tabs
|
||||||
value={activeView}
|
value={activeView}
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
|
|||||||
@@ -29,14 +29,6 @@ ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
|||||||
<Route path="/login" element={<Login />} />
|
<Route path="/login" element={<Login />} />
|
||||||
|
|
||||||
{/* authed routes */}
|
{/* authed routes */}
|
||||||
<Route
|
|
||||||
path="/app"
|
|
||||||
element={
|
|
||||||
<RequireAuth>
|
|
||||||
<Navigate to="/issues" replace />
|
|
||||||
</RequireAuth>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Route
|
<Route
|
||||||
path="/issues"
|
path="/issues"
|
||||||
element={
|
element={
|
||||||
|
|||||||
3
todo.md
3
todo.md
@@ -12,12 +12,9 @@
|
|||||||
|
|
||||||
# LOW PRIORITY
|
# LOW PRIORITY
|
||||||
|
|
||||||
- disable self-hosting stuff
|
|
||||||
- make closed source
|
|
||||||
- dedicated /register route (currently login/register are combined on /login)
|
- dedicated /register route (currently login/register are combined on /login)
|
||||||
- real logo
|
- real logo
|
||||||
- org settings
|
- org settings
|
||||||
- disable individual features
|
|
||||||
- manage issue types, default is [bug, feature]
|
- manage issue types, default is [bug, feature]
|
||||||
- create, edit, delete
|
- create, edit, delete
|
||||||
- assign icons to issue types (ensure each available icon is in EACH icon set)
|
- assign icons to issue types (ensure each available icon is in EACH icon set)
|
||||||
|
|||||||
Reference in New Issue
Block a user