feat: tiered costs feature + admin tier management + tag selection
Build & Push Docker Image / build (push) Successful in 6m52s

- costs: nullable notes (fix create without notes), drop renewalDate
  (schema + API + UI), gate POST/PATCH/DELETE to admin + costs feature
- feature middleware: admin-aware hasFeature + getEntitlements union;
  /auth/me and login return resolved entitlements
- users: tier enum (free/premium/enterprise) in create/update/list,
  admin UI tier select + tier badge
- tags: GET /tags/all, TagInput autocomplete in new/edit tool forms,
  feature suggestions on focus, query invalidation on create/update
- openapi: nullable ToolUpdate urls, ToolUpdate tier fields, listAllTags
- Dockerfile: push-force to drop renewal_date column
This commit is contained in:
opencode
2026-08-02 01:10:47 +02:00
parent cd4efd16f6
commit 01c70085db
27 changed files with 478 additions and 86 deletions
+9 -6
View File
@@ -2,7 +2,7 @@ import { useLocation } from "wouter";
import { useForm, useFieldArray } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import * as z from "zod";
import { useCreateTool, getListToolsQueryKey, getListCategoriesQueryKey, getListAllFeaturesQueryKey, getGetTopToolsQueryKey, getGetAnalyticsSummaryQueryKey } from "@workspace/api-client-react";
import { useCreateTool, getListToolsQueryKey, getListCategoriesQueryKey, getListAllFeaturesQueryKey, getListAllTagsQueryKey, getGetTopToolsQueryKey, getGetAnalyticsSummaryQueryKey } from "@workspace/api-client-react";
import { useQueryClient } from "@tanstack/react-query";
import { Layout } from "@/components/layout";
@@ -16,6 +16,7 @@ import { Wrench, Plus, X, ArrowLeft, LogIn } from "lucide-react";
import { Link } from "wouter";
import { CategoryCombobox } from "@/components/category-combobox";
import { FeatureInput } from "@/components/feature-input";
import { TagInput } from "@/components/tag-input";
import { useAuth } from "@/hooks/use-auth";
const toolSchema = z.object({
@@ -77,6 +78,7 @@ export default function ToolNew() {
queryClient.invalidateQueries({ queryKey: getListToolsQueryKey() });
queryClient.invalidateQueries({ queryKey: getListCategoriesQueryKey() });
queryClient.invalidateQueries({ queryKey: getListAllFeaturesQueryKey() });
queryClient.invalidateQueries({ queryKey: getListAllTagsQueryKey() });
queryClient.invalidateQueries({ queryKey: getGetTopToolsQueryKey() });
queryClient.invalidateQueries({ queryKey: getGetAnalyticsSummaryQueryKey() });
setLocation(`/tools/${newTool.id}`);
@@ -230,7 +232,7 @@ export default function ToolNew() {
<div className="flex justify-between items-center">
<div>
<h3 className="text-lg font-medium">Features</h3>
<p className="text-sm text-muted-foreground">List key capabilities. Start typing to see suggestions from existing tools.</p>
<p className="text-sm text-muted-foreground">List key capabilities. Existing features from other tools are selectable.</p>
</div>
<Button
type="button"
@@ -283,7 +285,7 @@ export default function ToolNew() {
<div className="flex justify-between items-center">
<div>
<h3 className="text-lg font-medium">Tags</h3>
<p className="text-sm text-muted-foreground">Keywords to help find this tool.</p>
<p className="text-sm text-muted-foreground">Keywords to help find this tool. Existing tags from other tools are selectable.</p>
</div>
<Button
type="button"
@@ -303,12 +305,13 @@ export default function ToolNew() {
control={form.control}
name={`tags.${index}.value`}
render={({ field }) => (
<FormItem className="flex items-center space-y-0 relative w-[150px]">
<FormItem className="flex items-center space-y-0 relative w-[200px]">
<FormControl>
<Input
<TagInput
placeholder="Tag"
className="pr-8 h-9 text-sm"
{...field}
onChange={field.onChange}
value={field.value ?? ""}
data-testid={`input-tag-${index}`}
/>
</FormControl>