@powerhousedao/contributor-billing 0.1.46 → 0.1.47

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"FolderTree.d.ts","sourceRoot":"","sources":["../../../../editors/builder-team-admin/components/FolderTree.tsx"],"names":[],"mappings":"AAgCA,iEAAiE;AACjE,MAAM,MAAM,UAAU,GAClB,cAAc,GACd,iBAAiB,GACjB,kBAAkB,GAClB,oBAAoB,GACpB,IAAI,CAAC;AA0GT,KAAK,eAAe,GAAG;IACrB,kBAAkB,CAAC,EAAE,CAAC,IAAI,EAAE,UAAU,KAAK,IAAI,CAAC;CACjD,CAAC;AAEF;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,EAAE,kBAAkB,EAAE,EAAE,eAAe,kDA0ZjE"}
1
+ {"version":3,"file":"FolderTree.d.ts","sourceRoot":"","sources":["../../../../editors/builder-team-admin/components/FolderTree.tsx"],"names":[],"mappings":"AAiCA,iEAAiE;AACjE,MAAM,MAAM,UAAU,GAClB,cAAc,GACd,iBAAiB,GACjB,kBAAkB,GAClB,oBAAoB,GACpB,IAAI,CAAC;AA0GT,KAAK,eAAe,GAAG;IACrB,kBAAkB,CAAC,EAAE,CAAC,IAAI,EAAE,UAAU,KAAK,IAAI,CAAC;CACjD,CAAC;AAEF;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,EAAE,kBAAkB,EAAE,EAAE,eAAe,kDA0ajE"}
@@ -6,7 +6,8 @@ import { useMemo, useState } from "react";
6
6
  const ICON_SIZE = 16;
7
7
  const EXPENSE_REPORTS_FOLDER_NAME = "Expense Reports";
8
8
  const SNAPSHOT_REPORTS_FOLDER_NAME = "Snapshot Reports";
9
- const RESOURCE_TEMPLATES_FOLDER_NAME = "Resource Templates";
9
+ const SERVICES_AND_OFFERINGS_FOLDER_NAME = "Services And Offerings";
10
+ const RESOURCE_TEMPLATES_FOLDER_NAME = "Products";
10
11
  const SERVICE_OFFERINGS_FOLDER_NAME = "Service Offerings";
11
12
  /**
12
13
  * Maps navigation section IDs to their corresponding document types.
@@ -52,7 +53,7 @@ const BASE_NAVIGATION_SECTIONS = [
52
53
  },
53
54
  {
54
55
  id: "resources-services",
55
- title: "Resources & Services",
56
+ title: "Service Offerings",
56
57
  icon: _jsx(Layers, { size: ICON_SIZE }),
57
58
  },
58
59
  {
@@ -122,20 +123,33 @@ export function FolderTree({ onCustomViewChange }) {
122
123
  const nodes = driveDocument.state.global.nodes;
123
124
  return nodes.find((node) => isFolderNodeKind(node) && node.name === SNAPSHOT_REPORTS_FOLDER_NAME);
124
125
  }, [driveDocument]);
125
- // Find the "Resource Templates" folder in the drive
126
- const resourceTemplatesFolder = useMemo(() => {
126
+ // Find the "Services And Offerings" parent folder in the drive (at root level)
127
+ const servicesAndOfferingsFolder = useMemo(() => {
127
128
  if (!driveDocument)
128
129
  return null;
129
130
  const nodes = driveDocument.state.global.nodes;
130
- return nodes.find((node) => isFolderNodeKind(node) && node.name === RESOURCE_TEMPLATES_FOLDER_NAME);
131
+ return nodes.find((node) => isFolderNodeKind(node) &&
132
+ node.name === SERVICES_AND_OFFERINGS_FOLDER_NAME &&
133
+ !node.parentFolder);
131
134
  }, [driveDocument]);
132
- // Find the "Service Offerings" folder in the drive
135
+ // Find the "Products" folder (inside Services And Offerings folder)
136
+ const resourceTemplatesFolder = useMemo(() => {
137
+ if (!driveDocument || !servicesAndOfferingsFolder)
138
+ return null;
139
+ const nodes = driveDocument.state.global.nodes;
140
+ return nodes.find((node) => isFolderNodeKind(node) &&
141
+ node.name === RESOURCE_TEMPLATES_FOLDER_NAME &&
142
+ node.parentFolder === servicesAndOfferingsFolder.id);
143
+ }, [driveDocument, servicesAndOfferingsFolder]);
144
+ // Find the "Service Offerings" folder (inside Services And Offerings folder)
133
145
  const serviceOfferingsFolder = useMemo(() => {
134
- if (!driveDocument)
146
+ if (!driveDocument || !servicesAndOfferingsFolder)
135
147
  return null;
136
148
  const nodes = driveDocument.state.global.nodes;
137
- return nodes.find((node) => isFolderNodeKind(node) && node.name === SERVICE_OFFERINGS_FOLDER_NAME);
138
- }, [driveDocument]);
149
+ return nodes.find((node) => isFolderNodeKind(node) &&
150
+ node.name === SERVICE_OFFERINGS_FOLDER_NAME &&
151
+ node.parentFolder === servicesAndOfferingsFolder.id);
152
+ }, [driveDocument, servicesAndOfferingsFolder]);
139
153
  // Build a set of all node IDs that are within the Expense Reports folder tree
140
154
  const expenseReportsNodeIds = useMemo(() => {
141
155
  const nodeIds = new Set();
@@ -425,7 +439,7 @@ export function FolderTree({ onCustomViewChange }) {
425
439
  showCreateDocumentModal(documentType);
426
440
  }
427
441
  };
428
- return (_jsx(SidebarProvider, { nodes: navigationSections, children: _jsx(Sidebar, { className: "pt-1", nodes: navigationSections, activeNodeId: activeNodeId, onActiveNodeChange: handleActiveNodeChange, sidebarTitle: "Builder Team Admin", showSearchBar: false, resizable: true, allowPinning: false, showStatusFilter: false, initialWidth: 256, defaultLevel: 2, handleOnTitleClick: () => {
442
+ return (_jsx(SidebarProvider, { nodes: navigationSections, children: _jsx(Sidebar, { className: "pt-1", nodes: navigationSections, activeNodeId: activeNodeId, onActiveNodeChange: handleActiveNodeChange, sidebarTitle: isOperator ? "Operator Team Admin" : "Builder Team Admin", showSearchBar: false, resizable: true, allowPinning: false, showStatusFilter: false, initialWidth: 256, defaultLevel: 2, handleOnTitleClick: () => {
429
443
  onCustomViewChange?.(null);
430
444
  setSelectedNode("");
431
445
  } }) }));
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Component for the Resources & Services custom view.
3
- * Shows two auto-generated folders: Resource Templates and Service Offerings.
4
- * Users can create powerhouse/resource-template docs in Resource Templates
3
+ * Shows folder structure: Services And Offerings > Products / Service Offerings.
4
+ * Users can create powerhouse/resource-template docs in Products
5
5
  * and powerhouse/service-offering docs in Service Offerings.
6
6
  */
7
7
  export declare function ResourcesServices(): import("react/jsx-runtime").JSX.Element;
@@ -1 +1 @@
1
- {"version":3,"file":"ResourcesServices.d.ts","sourceRoot":"","sources":["../../../../editors/builder-team-admin/components/ResourcesServices.tsx"],"names":[],"mappings":"AAmBA;;;;;GAKG;AACH,wBAAgB,iBAAiB,4CAgQhC"}
1
+ {"version":3,"file":"ResourcesServices.d.ts","sourceRoot":"","sources":["../../../../editors/builder-team-admin/components/ResourcesServices.tsx"],"names":[],"mappings":"AAoBA;;;;;GAKG;AACH,wBAAgB,iBAAiB,4CAwQhC"}
@@ -1,16 +1,17 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
1
+ import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { FileItem } from "@powerhousedao/design-system/connect";
3
3
  import { FolderItem } from "@powerhousedao/design-system/connect";
4
4
  import { isFolderNodeKind, isFileNodeKind, setSelectedNode, useSelectedNodePath, useNodesInSelectedDriveOrFolder, useUserPermissions, showCreateDocumentModal, } from "@powerhousedao/reactor-browser";
5
- import { useEffect, useRef, useState } from "react";
5
+ import { useEffect, useRef } from "react";
6
6
  import { Plus, FileText, Package } from "lucide-react";
7
7
  import { useResourcesServicesAutoPlacement } from "../hooks/useResourcesServicesAutoPlacement.js";
8
- const RESOURCE_TEMPLATES_FOLDER_NAME = "Resource Templates";
8
+ const SERVICES_AND_OFFERINGS_FOLDER_NAME = "Services And Offerings";
9
+ const RESOURCE_TEMPLATES_FOLDER_NAME = "Products";
9
10
  const SERVICE_OFFERINGS_FOLDER_NAME = "Service Offerings";
10
11
  /**
11
12
  * Component for the Resources & Services custom view.
12
- * Shows two auto-generated folders: Resource Templates and Service Offerings.
13
- * Users can create powerhouse/resource-template docs in Resource Templates
13
+ * Shows folder structure: Services And Offerings > Products / Service Offerings.
14
+ * Users can create powerhouse/resource-template docs in Products
14
15
  * and powerhouse/service-offering docs in Service Offerings.
15
16
  */
16
17
  export function ResourcesServices() {
@@ -19,9 +20,11 @@ export function ResourcesServices() {
19
20
  const nodesInCurrentFolder = useNodesInSelectedDriveOrFolder();
20
21
  const { isAllowedToCreateDocuments } = useUserPermissions();
21
22
  // Use the shared auto-placement hook - this handles:
22
- // 1. Creating the "Resource Templates" folder if it doesn't exist
23
- // 2. Creating the "Service Offerings" folder if it doesn't exist
24
- const { resourceTemplatesFolder, serviceOfferingsFolder, resourceTemplateDocuments, serviceOfferingDocuments, } = useResourcesServicesAutoPlacement();
23
+ // 1. Creating the "Services And Offerings" parent folder if it doesn't exist
24
+ // 2. Creating the "Products" subfolder if it doesn't exist
25
+ // 3. Creating the "Service Offerings" subfolder if it doesn't exist
26
+ // 4. Migrating existing documents from old folder structure
27
+ const { servicesAndOfferingsFolder, resourceTemplatesFolder, serviceOfferingsFolder, resourceTemplateDocuments, serviceOfferingDocuments, } = useResourcesServicesAutoPlacement();
25
28
  // Determine which folder we're currently in (if any)
26
29
  const currentFolderId = selectedNodePath.at(-1)?.id;
27
30
  const isInResourceTemplates = currentFolderId === resourceTemplatesFolder?.id;
@@ -29,17 +32,24 @@ export function ResourcesServices() {
29
32
  const isInRootView = !isInResourceTemplates && !isInServiceOfferings;
30
33
  // Navigate to root view initially (deselect any node)
31
34
  useEffect(() => {
32
- if (resourceTemplatesFolder &&
35
+ if (servicesAndOfferingsFolder &&
36
+ resourceTemplatesFolder &&
33
37
  serviceOfferingsFolder &&
34
38
  !hasNavigatedToFolder.current) {
35
39
  hasNavigatedToFolder.current = true;
36
40
  // Don't select any node so we show the root view with both folders
37
41
  setSelectedNode("");
38
42
  }
39
- }, [resourceTemplatesFolder, serviceOfferingsFolder]);
43
+ }, [
44
+ servicesAndOfferingsFolder,
45
+ resourceTemplatesFolder,
46
+ serviceOfferingsFolder,
47
+ ]);
40
48
  // Show loading state while folders are being created
41
- if (!resourceTemplatesFolder || !serviceOfferingsFolder) {
42
- return (_jsx("div", { className: "flex items-center justify-center h-64", children: _jsx("div", { className: "text-gray-500", children: "Setting up Resources & Services folders..." }) }));
49
+ if (!servicesAndOfferingsFolder ||
50
+ !resourceTemplatesFolder ||
51
+ !serviceOfferingsFolder) {
52
+ return (_jsx("div", { className: "flex items-center justify-center h-64", children: _jsxs("div", { className: "text-gray-500", children: ["Setting up ", SERVICES_AND_OFFERINGS_FOLDER_NAME, " folders..."] }) }));
43
53
  }
44
54
  // Handler for creating new documents
45
55
  const handleCreateDocument = (documentType, folderId) => {
@@ -54,10 +64,10 @@ export function ResourcesServices() {
54
64
  const fileNodes = nodesInCurrentFolder.filter((n) => isFileNodeKind(n));
55
65
  // Render the root view with both folder cards
56
66
  if (isInRootView) {
57
- return (_jsxs("div", { children: [_jsx("div", { className: "text-2xl font-bold text-center mb-6", children: "Resources & Services" }), _jsxs("div", { className: "space-y-6 px-6", children: [_jsx("p", { className: "text-gray-600 text-center mb-8", children: "Manage your resource templates and service offerings. Click on a folder to view or create documents." }), _jsxs("div", { className: "grid grid-cols-1 md:grid-cols-2 gap-6", children: [_jsxs("div", { className: "border border-gray-200 rounded-lg p-6 hover:border-blue-400 hover:shadow-md transition-all cursor-pointer", onClick: () => setSelectedNode(resourceTemplatesFolder.id), children: [_jsxs("div", { className: "flex items-center gap-3 mb-4", children: [_jsx("div", { className: "p-2 bg-blue-50 rounded-lg", children: _jsx(FileText, { className: "w-6 h-6 text-blue-600" }) }), _jsx("h3", { className: "text-lg font-semibold", children: RESOURCE_TEMPLATES_FOLDER_NAME })] }), _jsx("p", { className: "text-gray-600 text-sm mb-4", children: "Define resource templates that can be used across service offerings." }), _jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("span", { className: "text-sm text-gray-500", children: [resourceTemplateDocuments.length, " template", resourceTemplateDocuments.length !== 1 ? "s" : ""] }), isAllowedToCreateDocuments && (_jsxs("button", { type: "button", className: "flex items-center gap-1 text-sm text-blue-600 hover:text-blue-800", onClick: (e) => {
67
+ return (_jsxs("div", { children: [_jsx("div", { className: "text-2xl font-bold text-center mb-6", children: SERVICES_AND_OFFERINGS_FOLDER_NAME }), _jsxs("div", { className: "space-y-6 px-6", children: [_jsx("p", { className: "text-gray-600 text-center mb-8", children: "Manage your products and service offerings. Click on a folder to view or create documents." }), _jsxs("div", { className: "grid grid-cols-1 md:grid-cols-2 gap-6", children: [_jsxs("div", { className: "border border-gray-200 rounded-lg p-6 hover:border-blue-400 hover:shadow-md transition-all cursor-pointer", onClick: () => setSelectedNode(resourceTemplatesFolder.id), children: [_jsxs("div", { className: "flex items-center gap-3 mb-4", children: [_jsx("div", { className: "p-2 bg-blue-50 rounded-lg", children: _jsx(FileText, { className: "w-6 h-6 text-blue-600" }) }), _jsx("h3", { className: "text-lg font-semibold", children: RESOURCE_TEMPLATES_FOLDER_NAME })] }), _jsx("p", { className: "text-gray-600 text-sm mb-4", children: "Define products that can be used across service offerings." }), _jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("span", { className: "text-sm text-gray-500", children: [resourceTemplateDocuments.length, " product", resourceTemplateDocuments.length !== 1 ? "s" : ""] }), isAllowedToCreateDocuments && (_jsxs("button", { type: "button", className: "flex items-center gap-1 text-sm bg-blue-500 text-white px-3 py-1 rounded-md hover:bg-blue-600 shadow-sm transition-colors", onClick: (e) => {
58
68
  e.stopPropagation();
59
69
  handleCreateDocument("powerhouse/resource-template", resourceTemplatesFolder.id);
60
- }, children: [_jsx(Plus, { size: 14 }), "Add new"] }))] })] }), _jsxs("div", { className: "border border-gray-200 rounded-lg p-6 hover:border-indigo-400 hover:shadow-md transition-all cursor-pointer", onClick: () => setSelectedNode(serviceOfferingsFolder.id), children: [_jsxs("div", { className: "flex items-center gap-3 mb-4", children: [_jsx("div", { className: "p-2 bg-indigo-50 rounded-lg", children: _jsx(Package, { className: "w-6 h-6 text-indigo-600" }) }), _jsx("h3", { className: "text-lg font-semibold", children: SERVICE_OFFERINGS_FOLDER_NAME })] }), _jsx("p", { className: "text-gray-600 text-sm mb-4", children: "Create and manage service offerings with pricing tiers and options." }), _jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("span", { className: "text-sm text-gray-500", children: [serviceOfferingDocuments.length, " offering", serviceOfferingDocuments.length !== 1 ? "s" : ""] }), isAllowedToCreateDocuments && (_jsxs("button", { type: "button", className: "flex items-center gap-1 text-sm text-indigo-600 hover:text-indigo-800", onClick: (e) => {
70
+ }, children: [_jsx(Plus, { size: 14 }), "Add new"] }))] })] }), _jsxs("div", { className: "border border-gray-200 rounded-lg p-6 hover:border-indigo-400 hover:shadow-md transition-all cursor-pointer", onClick: () => setSelectedNode(serviceOfferingsFolder.id), children: [_jsxs("div", { className: "flex items-center gap-3 mb-4", children: [_jsx("div", { className: "p-2 bg-indigo-50 rounded-lg", children: _jsx(Package, { className: "w-6 h-6 text-indigo-600" }) }), _jsx("h3", { className: "text-lg font-semibold", children: SERVICE_OFFERINGS_FOLDER_NAME })] }), _jsx("p", { className: "text-gray-600 text-sm mb-4", children: "Create and manage service offerings with pricing tiers and options." }), _jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("span", { className: "text-sm text-gray-500", children: [serviceOfferingDocuments.length, " offering", serviceOfferingDocuments.length !== 1 ? "s" : ""] }), isAllowedToCreateDocuments && (_jsxs("button", { type: "button", className: "flex items-center gap-1 text-sm bg-indigo-500 text-white px-3 py-1 rounded-md hover:bg-indigo-600 shadow-sm transition-colors", onClick: (e) => {
61
71
  e.stopPropagation();
62
72
  handleCreateDocument("powerhouse/service-offering", serviceOfferingsFolder.id);
63
73
  }, children: [_jsx(Plus, { size: 14 }), "Add new"] }))] })] })] })] })] }));
@@ -72,7 +82,5 @@ export function ResourcesServices() {
72
82
  const hasFolders = folderNodes.length > 0;
73
83
  const hasFiles = fileNodes.length > 0;
74
84
  const isEmpty = !hasFolders && !hasFiles;
75
- return (_jsxs("div", { children: [_jsx("div", { className: "text-2xl font-bold text-center mb-4", children: currentFolderName }), _jsxs("div", { className: "space-y-6 px-6", children: [_jsxs("div", { className: "flex h-9 flex-row items-center gap-2 text-gray-500 border-b border-gray-200 pb-3", children: [_jsx("div", { className: "transition-colors hover:text-gray-800 cursor-pointer", onClick: () => setSelectedNode(""), role: "button", children: "Resources & Services" }), _jsx("span", { children: "/" }), _jsx("div", { className: "text-gray-800", children: currentFolderName }), _jsx("span", { children: "/" }), isAllowedToCreateDocuments && (_jsxs("button", { type: "button", className: "ml-1 flex items-center justify-center gap-2 rounded-md bg-gray-50 px-2 py-1.5 transition-colors hover:bg-gray-200 hover:text-gray-800", onClick: () => showCreateDocumentModal(documentType), children: [_jsx(Plus, { size: 14 }), "Add new"] }))] }), hasFolders && (_jsxs("div", { children: [_jsx("h3", { className: "mb-2 text-sm font-bold text-gray-600", children: "Folders" }), _jsx("div", { className: "flex flex-wrap gap-4", children: folderNodes.map((folderNode) => (_jsx(FolderItem, { folderNode: folderNode }, folderNode.id))) })] })), hasFiles && (_jsxs("div", { children: [_jsx("h3", { className: "mb-2 text-sm font-semibold text-gray-600", children: "Documents" }), _jsx("div", { className: "flex flex-wrap gap-4", children: fileNodes.map((fileNode) => (_jsx(FileItem, { fileNode: fileNode }, fileNode.id))) })] })), isEmpty && (_jsxs("div", { className: "flex flex-col items-center justify-center py-12 text-center", children: [_jsx("div", { className: "text-gray-400 mb-2", children: isInResourceTemplates ? (_jsx(FileText, { className: "w-16 h-16 mx-auto" })) : (_jsx(Package, { className: "w-16 h-16 mx-auto" })) }), _jsxs("p", { className: "text-gray-500 text-sm", children: ["No ", currentFolderName.toLowerCase(), " yet.", isAllowedToCreateDocuments && (_jsxs(_Fragment, { children: [" ", "Click \"Add new\" to create your first", " ", isInResourceTemplates
76
- ? "resource template"
77
- : "service offering", "."] }))] })] }))] })] }));
85
+ return (_jsxs("div", { children: [_jsx("div", { className: "text-2xl font-bold text-center mb-4", children: currentFolderName }), _jsxs("div", { className: "space-y-6 px-6", children: [_jsxs("div", { className: "flex h-9 flex-row items-center gap-2 text-gray-500 border-b border-gray-200 pb-3", children: [_jsx("div", { className: "transition-colors hover:text-gray-800 cursor-pointer", onClick: () => setSelectedNode(""), role: "button", children: SERVICES_AND_OFFERINGS_FOLDER_NAME }), _jsx("span", { children: "/" }), _jsx("div", { className: "text-gray-800", children: currentFolderName }), _jsx("span", { children: "/" }), isAllowedToCreateDocuments && (_jsxs("button", { type: "button", className: "ml-1 flex items-center justify-center gap-2 rounded-md bg-gray-50 px-2 py-1.5 transition-colors hover:bg-gray-200 hover:text-gray-800", onClick: () => showCreateDocumentModal(documentType), children: [_jsx(Plus, { size: 14 }), "Add new"] }))] }), hasFolders && (_jsxs("div", { children: [_jsx("h3", { className: "mb-2 text-sm font-bold text-gray-600", children: "Folders" }), _jsx("div", { className: "flex flex-wrap gap-4", children: folderNodes.map((folderNode) => (_jsx(FolderItem, { folderNode: folderNode }, folderNode.id))) })] })), hasFiles && (_jsxs("div", { children: [_jsx("h3", { className: "mb-2 text-sm font-semibold text-gray-600", children: "Documents" }), _jsx("div", { className: "flex flex-wrap gap-4", children: fileNodes.map((fileNode) => (_jsx(FileItem, { fileNode: fileNode }, fileNode.id))) })] })), isEmpty && (_jsxs("div", { className: "flex flex-col items-center justify-center py-12 text-center", children: [_jsx("div", { className: "text-gray-400 mb-2", children: isInResourceTemplates ? (_jsx(FileText, { className: "w-16 h-16 mx-auto" })) : (_jsx(Package, { className: "w-16 h-16 mx-auto" })) }), _jsxs("p", { className: "text-gray-500 text-sm", children: ["No ", currentFolderName.toLowerCase(), " yet.", isAllowedToCreateDocuments && (_jsxs(_Fragment, { children: [" ", "Click \"Add new\" to create your first", " ", isInResourceTemplates ? "product" : "service offering", "."] }))] })] }))] })] }));
78
86
  }
@@ -1,26 +1,35 @@
1
1
  import type { FolderNode, FileNode } from "document-drive";
2
2
  interface UseResourcesServicesAutoPlacementResult {
3
- /** The Resource Templates folder node, or null if it doesn't exist yet */
3
+ /** The parent "Services And Offerings" folder node, or null if it doesn't exist yet */
4
+ servicesAndOfferingsFolder: FolderNode | null;
5
+ /** The Products folder node (inside Services And Offerings), or null if it doesn't exist yet */
4
6
  resourceTemplatesFolder: FolderNode | null;
5
- /** The Service Offerings folder node, or null if it doesn't exist yet */
7
+ /** The Service Offerings folder node (inside Services And Offerings), or null if it doesn't exist yet */
6
8
  serviceOfferingsFolder: FolderNode | null;
7
- /** Set of all node IDs within the Resource Templates folder tree */
9
+ /** Set of all node IDs within the Products folder tree */
8
10
  resourceTemplatesNodeIds: Set<string>;
9
11
  /** Set of all node IDs within the Service Offerings folder tree */
10
12
  serviceOfferingsNodeIds: Set<string>;
11
- /** All resource template documents within the Resource Templates folder */
13
+ /** All resource template documents within the Products folder */
12
14
  resourceTemplateDocuments: FileNode[];
13
15
  /** All service offering documents within the Service Offerings folder */
14
16
  serviceOfferingDocuments: FileNode[];
15
17
  }
16
18
  /**
17
- * Hook that handles automatic creation of "Resource Templates" and "Service Offerings" folders
18
- * for the Resources & Services section.
19
+ * Hook that handles automatic creation of "Services And Offerings" parent folder
20
+ * with "Products" and "Service Offerings" subfolders, and migrates existing documents
21
+ * from old folder structure.
22
+ *
23
+ * Folder structure:
24
+ * - Services And Offerings (parent folder)
25
+ * - Products (for powerhouse/resource-template docs)
26
+ * - Service Offerings (for powerhouse/service-offering docs)
19
27
  *
20
28
  * This hook:
21
- * 1. Creates the "Resource Templates" folder if it doesn't exist
22
- * 2. Creates the "Service Offerings" folder if it doesn't exist
23
- * 3. Provides access to documents within each folder
29
+ * 1. Creates the folder structure if it doesn't exist
30
+ * 2. Migrates existing documents from old folders to new structure
31
+ * 3. Deletes old folders after migration
32
+ * 4. Provides access to documents within each folder
24
33
  */
25
34
  export declare function useResourcesServicesAutoPlacement(): UseResourcesServicesAutoPlacementResult;
26
35
  export {};
@@ -1 +1 @@
1
- {"version":3,"file":"useResourcesServicesAutoPlacement.d.ts","sourceRoot":"","sources":["../../../../editors/builder-team-admin/hooks/useResourcesServicesAutoPlacement.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAQ,MAAM,gBAAgB,CAAC;AAKjE,UAAU,uCAAuC;IAC/C,0EAA0E;IAC1E,uBAAuB,EAAE,UAAU,GAAG,IAAI,CAAC;IAC3C,yEAAyE;IACzE,sBAAsB,EAAE,UAAU,GAAG,IAAI,CAAC;IAC1C,oEAAoE;IACpE,wBAAwB,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACtC,mEAAmE;IACnE,uBAAuB,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACrC,2EAA2E;IAC3E,yBAAyB,EAAE,QAAQ,EAAE,CAAC;IACtC,yEAAyE;IACzE,wBAAwB,EAAE,QAAQ,EAAE,CAAC;CACtC;AAED;;;;;;;;GAQG;AACH,wBAAgB,iCAAiC,IAAI,uCAAuC,CAiJ3F"}
1
+ {"version":3,"file":"useResourcesServicesAutoPlacement.d.ts","sourceRoot":"","sources":["../../../../editors/builder-team-admin/hooks/useResourcesServicesAutoPlacement.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAQ,MAAM,gBAAgB,CAAC;AAkBjE,UAAU,uCAAuC;IAC/C,uFAAuF;IACvF,0BAA0B,EAAE,UAAU,GAAG,IAAI,CAAC;IAC9C,gGAAgG;IAChG,uBAAuB,EAAE,UAAU,GAAG,IAAI,CAAC;IAC3C,yGAAyG;IACzG,sBAAsB,EAAE,UAAU,GAAG,IAAI,CAAC;IAC1C,0DAA0D;IAC1D,wBAAwB,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACtC,mEAAmE;IACnE,uBAAuB,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACrC,iEAAiE;IACjE,yBAAyB,EAAE,QAAQ,EAAE,CAAC;IACtC,yEAAyE;IACzE,wBAAwB,EAAE,QAAQ,EAAE,CAAC;CACtC;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,iCAAiC,IAAI,uCAAuC,CAuU3F"}
@@ -1,38 +1,89 @@
1
- import { useEffect, useMemo, useRef } from "react";
2
- import { isFolderNodeKind, isFileNodeKind, addFolder, useSelectedDrive, useDocumentsInSelectedDrive, } from "@powerhousedao/reactor-browser";
3
- const RESOURCE_TEMPLATES_FOLDER_NAME = "Resource Templates";
1
+ import { useEffect, useMemo } from "react";
2
+ import { isFolderNodeKind, isFileNodeKind, addFolder, useSelectedDrive, useNodeActions, dispatchActions, } from "@powerhousedao/reactor-browser";
3
+ import { deleteNode } from "document-drive";
4
+ const SERVICES_AND_OFFERINGS_FOLDER_NAME = "Services And Offerings";
5
+ const PRODUCTS_FOLDER_NAME = "Products";
4
6
  const SERVICE_OFFERINGS_FOLDER_NAME = "Service Offerings";
7
+ // Old folder names that might exist from previous structure (for migration)
8
+ const OLD_RESOURCE_TEMPLATES_FOLDER_NAME = "Resource Templates";
9
+ // Module-level tracking to prevent duplicate folder creation across all hook instances
10
+ const globalCreationState = {
11
+ createdServicesAndOfferingsFolderForDrives: new Set(),
12
+ createdProductsFolderForDrives: new Set(),
13
+ createdServiceOfferingsFolderForDrives: new Set(),
14
+ processedDocs: new Map(), // driveId -> Set of doc IDs processed
15
+ migratedOldFolders: new Set(), // driveId where migration has been completed
16
+ };
5
17
  /**
6
- * Hook that handles automatic creation of "Resource Templates" and "Service Offerings" folders
7
- * for the Resources & Services section.
18
+ * Hook that handles automatic creation of "Services And Offerings" parent folder
19
+ * with "Products" and "Service Offerings" subfolders, and migrates existing documents
20
+ * from old folder structure.
21
+ *
22
+ * Folder structure:
23
+ * - Services And Offerings (parent folder)
24
+ * - Products (for powerhouse/resource-template docs)
25
+ * - Service Offerings (for powerhouse/service-offering docs)
8
26
  *
9
27
  * This hook:
10
- * 1. Creates the "Resource Templates" folder if it doesn't exist
11
- * 2. Creates the "Service Offerings" folder if it doesn't exist
12
- * 3. Provides access to documents within each folder
28
+ * 1. Creates the folder structure if it doesn't exist
29
+ * 2. Migrates existing documents from old folders to new structure
30
+ * 3. Deletes old folders after migration
31
+ * 4. Provides access to documents within each folder
13
32
  */
14
33
  export function useResourcesServicesAutoPlacement() {
15
34
  const [driveDocument] = useSelectedDrive();
16
- const documentsInDrive = useDocumentsInSelectedDrive();
17
- // Track folder creation to prevent duplicates
18
- const hasCreatedResourceTemplatesFolder = useRef(false);
19
- const hasCreatedServiceOfferingsFolder = useRef(false);
20
- // Find the "Resource Templates" folder in the drive
21
- const resourceTemplatesFolder = useMemo(() => {
35
+ const { onMoveNode } = useNodeActions();
36
+ const driveId = driveDocument?.header.id;
37
+ // Initialize module-level tracking sets for this drive if needed
38
+ if (driveId && !globalCreationState.processedDocs.has(driveId)) {
39
+ globalCreationState.processedDocs.set(driveId, new Set());
40
+ }
41
+ // Find the "Services And Offerings" parent folder in the drive (at root level)
42
+ const servicesAndOfferingsFolder = useMemo(() => {
22
43
  if (!driveDocument)
23
44
  return null;
24
45
  const nodes = driveDocument.state.global.nodes;
25
46
  return (nodes.find((node) => isFolderNodeKind(node) &&
26
- node.name === RESOURCE_TEMPLATES_FOLDER_NAME) ?? null);
47
+ node.name === SERVICES_AND_OFFERINGS_FOLDER_NAME &&
48
+ !node.parentFolder) ?? null);
27
49
  }, [driveDocument]);
28
- // Find the "Service Offerings" folder in the drive
50
+ // Find the "Products" folder (must be inside Services And Offerings folder)
51
+ const resourceTemplatesFolder = useMemo(() => {
52
+ if (!driveDocument || !servicesAndOfferingsFolder)
53
+ return null;
54
+ const nodes = driveDocument.state.global.nodes;
55
+ return (nodes.find((node) => isFolderNodeKind(node) &&
56
+ node.name === PRODUCTS_FOLDER_NAME &&
57
+ node.parentFolder === servicesAndOfferingsFolder.id) ?? null);
58
+ }, [driveDocument, servicesAndOfferingsFolder]);
59
+ // Find the "Service Offerings" folder (must be inside Services And Offerings folder)
29
60
  const serviceOfferingsFolder = useMemo(() => {
61
+ if (!driveDocument || !servicesAndOfferingsFolder)
62
+ return null;
63
+ const nodes = driveDocument.state.global.nodes;
64
+ return (nodes.find((node) => isFolderNodeKind(node) &&
65
+ node.name === SERVICE_OFFERINGS_FOLDER_NAME &&
66
+ node.parentFolder === servicesAndOfferingsFolder.id) ?? null);
67
+ }, [driveDocument, servicesAndOfferingsFolder]);
68
+ // Find old folders that might exist from previous structure (at root level)
69
+ const oldResourceTemplatesFolder = useMemo(() => {
30
70
  if (!driveDocument)
31
71
  return null;
32
72
  const nodes = driveDocument.state.global.nodes;
33
- return (nodes.find((node) => isFolderNodeKind(node) && node.name === SERVICE_OFFERINGS_FOLDER_NAME) ?? null);
73
+ return (nodes.find((node) => isFolderNodeKind(node) &&
74
+ (node.name === OLD_RESOURCE_TEMPLATES_FOLDER_NAME ||
75
+ node.name === PRODUCTS_FOLDER_NAME) &&
76
+ !node.parentFolder) ?? null);
34
77
  }, [driveDocument]);
35
- // Build a set of all node IDs within the Resource Templates folder tree
78
+ const oldServiceOfferingsFolder = useMemo(() => {
79
+ if (!driveDocument)
80
+ return null;
81
+ const nodes = driveDocument.state.global.nodes;
82
+ return (nodes.find((node) => isFolderNodeKind(node) &&
83
+ node.name === SERVICE_OFFERINGS_FOLDER_NAME &&
84
+ !node.parentFolder) ?? null);
85
+ }, [driveDocument]);
86
+ // Build a set of all node IDs within the Products folder tree
36
87
  const resourceTemplatesNodeIds = useMemo(() => {
37
88
  const nodeIds = new Set();
38
89
  if (!resourceTemplatesFolder || !driveDocument)
@@ -76,7 +127,7 @@ export function useResourcesServicesAutoPlacement() {
76
127
  collectNodeIds(serviceOfferingsFolder.id);
77
128
  return nodeIds;
78
129
  }, [serviceOfferingsFolder, driveDocument]);
79
- // Get resource template documents within the Resource Templates folder
130
+ // Get resource template documents within the Products folder
80
131
  const resourceTemplateDocuments = useMemo(() => {
81
132
  if (!driveDocument)
82
133
  return [];
@@ -92,27 +143,117 @@ export function useResourcesServicesAutoPlacement() {
92
143
  node.documentType === "powerhouse/service-offering" &&
93
144
  serviceOfferingsNodeIds.has(node.id));
94
145
  }, [driveDocument, serviceOfferingsNodeIds]);
95
- // Create Resource Templates folder if it doesn't exist
146
+ // Step 1: Create "Services And Offerings" parent folder if it doesn't exist
147
+ useEffect(() => {
148
+ if (!driveId || servicesAndOfferingsFolder)
149
+ return;
150
+ if (globalCreationState.createdServicesAndOfferingsFolderForDrives.has(driveId))
151
+ return;
152
+ globalCreationState.createdServicesAndOfferingsFolderForDrives.add(driveId);
153
+ void addFolder(driveId, SERVICES_AND_OFFERINGS_FOLDER_NAME);
154
+ }, [driveId, servicesAndOfferingsFolder]);
155
+ // Step 2: Create "Products" subfolder if it doesn't exist (after parent exists)
96
156
  useEffect(() => {
97
- if (!driveDocument ||
98
- resourceTemplatesFolder ||
99
- hasCreatedResourceTemplatesFolder.current)
157
+ if (!driveId || !servicesAndOfferingsFolder || resourceTemplatesFolder)
100
158
  return;
101
- hasCreatedResourceTemplatesFolder.current = true;
102
- const driveId = driveDocument.header.id;
103
- void addFolder(driveId, RESOURCE_TEMPLATES_FOLDER_NAME);
104
- }, [driveDocument, resourceTemplatesFolder]);
105
- // Create Service Offerings folder if it doesn't exist
159
+ if (globalCreationState.createdProductsFolderForDrives.has(driveId))
160
+ return;
161
+ globalCreationState.createdProductsFolderForDrives.add(driveId);
162
+ void addFolder(driveId, PRODUCTS_FOLDER_NAME, servicesAndOfferingsFolder.id);
163
+ }, [driveId, servicesAndOfferingsFolder, resourceTemplatesFolder]);
164
+ // Step 3: Create "Service Offerings" subfolder if it doesn't exist (after parent exists)
106
165
  useEffect(() => {
107
- if (!driveDocument ||
108
- serviceOfferingsFolder ||
109
- hasCreatedServiceOfferingsFolder.current)
166
+ if (!driveId || !servicesAndOfferingsFolder || serviceOfferingsFolder)
167
+ return;
168
+ if (globalCreationState.createdServiceOfferingsFolderForDrives.has(driveId))
169
+ return;
170
+ globalCreationState.createdServiceOfferingsFolderForDrives.add(driveId);
171
+ void addFolder(driveId, SERVICE_OFFERINGS_FOLDER_NAME, servicesAndOfferingsFolder.id);
172
+ }, [driveId, servicesAndOfferingsFolder, serviceOfferingsFolder]);
173
+ // Step 4: Migrate documents from old folders to new structure and delete old folders
174
+ useEffect(() => {
175
+ if (!driveId ||
176
+ !driveDocument ||
177
+ !resourceTemplatesFolder ||
178
+ !serviceOfferingsFolder)
179
+ return;
180
+ if (globalCreationState.migratedOldFolders.has(driveId))
110
181
  return;
111
- hasCreatedServiceOfferingsFolder.current = true;
112
- const driveId = driveDocument.header.id;
113
- void addFolder(driveId, SERVICE_OFFERINGS_FOLDER_NAME);
114
- }, [driveDocument, serviceOfferingsFolder]);
182
+ const allNodes = driveDocument.state.global.nodes;
183
+ const processedDocs = globalCreationState.processedDocs.get(driveId);
184
+ if (!processedDocs)
185
+ return;
186
+ // Find all resource template documents that are NOT in the correct folder
187
+ const resourceTemplatesToMigrate = allNodes.filter((node) => isFileNodeKind(node) &&
188
+ node.documentType === "powerhouse/resource-template" &&
189
+ !resourceTemplatesNodeIds.has(node.id) &&
190
+ !processedDocs.has(node.id));
191
+ // Find all service offering documents that are NOT in the correct folder
192
+ const serviceOfferingsToMigrate = allNodes.filter((node) => isFileNodeKind(node) &&
193
+ node.documentType === "powerhouse/service-offering" &&
194
+ !serviceOfferingsNodeIds.has(node.id) &&
195
+ !processedDocs.has(node.id));
196
+ // Move resource templates to Products folder
197
+ for (const fileNode of resourceTemplatesToMigrate) {
198
+ processedDocs.add(fileNode.id);
199
+ onMoveNode(fileNode, resourceTemplatesFolder).catch((error) => {
200
+ console.error(`Failed to migrate resource template:`, error);
201
+ processedDocs.delete(fileNode.id);
202
+ });
203
+ }
204
+ // Move service offerings to Service Offerings folder
205
+ for (const fileNode of serviceOfferingsToMigrate) {
206
+ processedDocs.add(fileNode.id);
207
+ onMoveNode(fileNode, serviceOfferingsFolder).catch((error) => {
208
+ console.error(`Failed to migrate service offering:`, error);
209
+ processedDocs.delete(fileNode.id);
210
+ });
211
+ }
212
+ // Delete old folders if they exist and are empty (after migration)
213
+ const checkAndDeleteOldFolders = () => {
214
+ // Check if old resource templates folder is empty and delete it
215
+ if (oldResourceTemplatesFolder) {
216
+ const childrenInOldResourceTemplates = allNodes.filter((node) => (isFolderNodeKind(node) || isFileNodeKind(node)) &&
217
+ node.parentFolder === oldResourceTemplatesFolder.id);
218
+ if (childrenInOldResourceTemplates.length === 0) {
219
+ dispatchActions(deleteNode({ id: oldResourceTemplatesFolder.id }), driveId).catch((error) => {
220
+ console.error(`Failed to delete old Resource Templates folder:`, error);
221
+ });
222
+ }
223
+ }
224
+ // Check if old service offerings folder is empty and delete it
225
+ if (oldServiceOfferingsFolder) {
226
+ const childrenInOldServiceOfferings = allNodes.filter((node) => (isFolderNodeKind(node) || isFileNodeKind(node)) &&
227
+ node.parentFolder === oldServiceOfferingsFolder.id);
228
+ if (childrenInOldServiceOfferings.length === 0) {
229
+ dispatchActions(deleteNode({ id: oldServiceOfferingsFolder.id }), driveId).catch((error) => {
230
+ console.error(`Failed to delete old Service Offerings folder:`, error);
231
+ });
232
+ }
233
+ }
234
+ globalCreationState.migratedOldFolders.add(driveId);
235
+ };
236
+ // Delay deletion to allow migrations to complete
237
+ if (resourceTemplatesToMigrate.length === 0 &&
238
+ serviceOfferingsToMigrate.length === 0) {
239
+ checkAndDeleteOldFolders();
240
+ }
241
+ else {
242
+ setTimeout(checkAndDeleteOldFolders, 1000);
243
+ }
244
+ }, [
245
+ driveId,
246
+ driveDocument,
247
+ resourceTemplatesFolder,
248
+ serviceOfferingsFolder,
249
+ resourceTemplatesNodeIds,
250
+ serviceOfferingsNodeIds,
251
+ oldResourceTemplatesFolder,
252
+ oldServiceOfferingsFolder,
253
+ onMoveNode,
254
+ ]);
115
255
  return {
256
+ servicesAndOfferingsFolder,
116
257
  resourceTemplatesFolder,
117
258
  serviceOfferingsFolder,
118
259
  resourceTemplatesNodeIds,
package/dist/style.css CHANGED
@@ -80,7 +80,6 @@
80
80
  --color-indigo-500: oklch(58.5% 0.233 277.117);
81
81
  --color-indigo-600: oklch(51.1% 0.262 276.966);
82
82
  --color-indigo-700: oklch(45.7% 0.24 277.023);
83
- --color-indigo-800: oklch(39.8% 0.195 277.366);
84
83
  --color-violet-50: oklch(96.9% 0.016 293.756);
85
84
  --color-violet-500: oklch(60.6% 0.25 292.717);
86
85
  --color-violet-600: oklch(54.1% 0.281 293.009);
@@ -2672,13 +2671,6 @@
2672
2671
  }
2673
2672
  }
2674
2673
  }
2675
- .hover\:text-indigo-800 {
2676
- &:hover {
2677
- @media (hover: hover) {
2678
- color: var(--color-indigo-800);
2679
- }
2680
- }
2681
- }
2682
2674
  .hover\:text-red-500 {
2683
2675
  &:hover {
2684
2676
  @media (hover: hover) {
@@ -22,7 +22,7 @@ export const getResolvers = (subgraph) => {
22
22
  if (operatorId && state.operatorId !== operatorId) {
23
23
  return [];
24
24
  }
25
- return [mapResourceTemplateState(state)];
25
+ return [mapResourceTemplateState(state, doc)];
26
26
  }
27
27
  }
28
28
  catch {
@@ -59,7 +59,7 @@ export const getResolvers = (subgraph) => {
59
59
  if (operatorId && state.operatorId !== operatorId) {
60
60
  continue;
61
61
  }
62
- resourceTemplates.push(mapResourceTemplateState(state));
62
+ resourceTemplates.push(mapResourceTemplateState(state, doc));
63
63
  }
64
64
  }
65
65
  }
@@ -93,7 +93,7 @@ export const getResolvers = (subgraph) => {
93
93
  state.resourceTemplateId !== resourceTemplateId) {
94
94
  return [];
95
95
  }
96
- return [mapServiceOfferingState(state)];
96
+ return [mapServiceOfferingState(state, doc)];
97
97
  }
98
98
  }
99
99
  catch {
@@ -135,7 +135,7 @@ export const getResolvers = (subgraph) => {
135
135
  state.resourceTemplateId !== resourceTemplateId) {
136
136
  continue;
137
137
  }
138
- serviceOfferings.push(mapServiceOfferingState(state));
138
+ serviceOfferings.push(mapServiceOfferingState(state, doc));
139
139
  }
140
140
  }
141
141
  }
@@ -151,9 +151,9 @@ export const getResolvers = (subgraph) => {
151
151
  /**
152
152
  * Map ResourceTemplateState from document model to GraphQL response
153
153
  */
154
- function mapResourceTemplateState(state) {
154
+ function mapResourceTemplateState(state, doc) {
155
155
  return {
156
- id: state.id,
156
+ id: doc.header.id,
157
157
  operatorId: state.operatorId,
158
158
  title: state.title,
159
159
  summary: state.summary,
@@ -214,9 +214,9 @@ function mapResourceTemplateState(state) {
214
214
  /**
215
215
  * Map ServiceOfferingState from document model to GraphQL response
216
216
  */
217
- function mapServiceOfferingState(state) {
217
+ function mapServiceOfferingState(state, doc) {
218
218
  return {
219
- id: state.id,
219
+ id: doc.header.id,
220
220
  operatorId: state.operatorId,
221
221
  resourceTemplateId: state.resourceTemplateId || null,
222
222
  title: state.title,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@powerhousedao/contributor-billing",
3
3
  "description": "Document models that help contributors of open organisations get paid anonymously for their work on a monthly basis.",
4
- "version": "0.1.46",
4
+ "version": "0.1.47",
5
5
  "license": "AGPL-3.0-only",
6
6
  "type": "module",
7
7
  "files": [