@powerhousedao/builder-profile 0.0.5 → 0.0.7
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.
- package/dist/document-models/builder-profile/gen/document-schema.d.ts +64 -64
- package/dist/document-models/builder-profile/gen/document-schema.d.ts.map +1 -1
- package/dist/document-models/builder-profile/gen/schema/types.d.ts +10 -10
- package/dist/document-models/builder-profile/gen/schema/types.d.ts.map +1 -1
- package/dist/document-models/document-models.d.ts.map +1 -1
- package/dist/document-models/document-models.js +1 -3
- package/dist/editors/builder-profile/components/ContributorsSection.d.ts.map +1 -1
- package/dist/editors/builder-profile/components/ContributorsSection.js +141 -32
- package/dist/editors/builder-profile/components/markdown-editor.d.ts +12 -0
- package/dist/editors/builder-profile/components/markdown-editor.d.ts.map +1 -0
- package/dist/editors/builder-profile/components/markdown-editor.js +133 -0
- package/dist/editors/builder-profile/editor.d.ts.map +1 -1
- package/dist/editors/builder-profile/editor.js +2 -5
- package/dist/editors/builder-profile/hooks/useRemoteBuilderProfiles.d.ts +20 -0
- package/dist/editors/builder-profile/hooks/useRemoteBuilderProfiles.d.ts.map +1 -0
- package/dist/editors/builder-profile/hooks/useRemoteBuilderProfiles.js +57 -0
- package/dist/editors/builder-profile/utils/graphql-client.d.ts +41 -0
- package/dist/editors/builder-profile/utils/graphql-client.d.ts.map +1 -0
- package/dist/editors/builder-profile/utils/graphql-client.js +180 -0
- package/dist/style.css +100 -0
- package/package.json +4 -2
|
@@ -1,7 +1,89 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useMemo, useCallback } from "react";
|
|
2
|
+
import { useMemo, useCallback, useState, useEffect } from "react";
|
|
3
3
|
import { useDrives, useGetDocuments } from "@powerhousedao/reactor-browser";
|
|
4
4
|
import { ObjectSetTable, PHIDInput, } from "@powerhousedao/document-engineering";
|
|
5
|
+
import { useRemoteBuilderProfiles } from "../hooks/useRemoteBuilderProfiles.js";
|
|
6
|
+
/**
|
|
7
|
+
* Wrapper component for PHIDInput that properly tracks selected PHID
|
|
8
|
+
* and handles saving on blur/enter with the correct PHID value.
|
|
9
|
+
*
|
|
10
|
+
* Key insight from the PHIDInput library:
|
|
11
|
+
* - onChange is called with the PHID value when user selects from dropdown (click or Enter on highlighted item)
|
|
12
|
+
* - After dropdown selection, the input is re-focused so onBlur doesn't fire
|
|
13
|
+
* - Therefore we must save immediately in onChange when a valid PHID is selected
|
|
14
|
+
*/
|
|
15
|
+
function ContributorPHIDInput({ initialPhid, options, onSave, fetchOptionsCallback, }) {
|
|
16
|
+
// Track the current input text for manual entry lookup
|
|
17
|
+
const [inputText, setInputText] = useState("");
|
|
18
|
+
// Track if we already saved to prevent duplicate saves
|
|
19
|
+
const [hasSaved, setHasSaved] = useState(false);
|
|
20
|
+
// Reset state when initialPhid changes (switching between rows)
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
setInputText("");
|
|
23
|
+
setHasSaved(false);
|
|
24
|
+
}, [initialPhid]);
|
|
25
|
+
// Find PHID by name or return the input if it looks like a PHID
|
|
26
|
+
const findPhidByInput = useCallback((input) => {
|
|
27
|
+
const trimmed = input.trim();
|
|
28
|
+
if (!trimmed)
|
|
29
|
+
return null;
|
|
30
|
+
const lowerInput = trimmed.toLowerCase();
|
|
31
|
+
// Check if input matches a profile name exactly (case-insensitive)
|
|
32
|
+
const exactMatchByName = options.find((opt) => opt.label.toLowerCase() === lowerInput);
|
|
33
|
+
if (exactMatchByName)
|
|
34
|
+
return exactMatchByName.id;
|
|
35
|
+
// Check if input matches a profile name partially (first match that starts with input)
|
|
36
|
+
const partialMatchByName = options.find((opt) => opt.label.toLowerCase().startsWith(lowerInput));
|
|
37
|
+
if (partialMatchByName)
|
|
38
|
+
return partialMatchByName.id;
|
|
39
|
+
// Check if only one option contains the input (unambiguous match)
|
|
40
|
+
const containsMatches = options.filter((opt) => opt.label.toLowerCase().includes(lowerInput));
|
|
41
|
+
if (containsMatches.length === 1)
|
|
42
|
+
return containsMatches[0].id;
|
|
43
|
+
// Check if input matches a profile ID
|
|
44
|
+
const matchById = options.find((opt) => opt.id.toLowerCase() === lowerInput);
|
|
45
|
+
if (matchById)
|
|
46
|
+
return matchById.id;
|
|
47
|
+
// If input looks like a UUID, return it directly
|
|
48
|
+
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
49
|
+
if (uuidRegex.test(trimmed))
|
|
50
|
+
return trimmed;
|
|
51
|
+
return null;
|
|
52
|
+
}, [options]);
|
|
53
|
+
// Check if a value is a known PHID from options
|
|
54
|
+
const isKnownPhid = useCallback((value) => {
|
|
55
|
+
return options.some((opt) => opt.id === value);
|
|
56
|
+
}, [options]);
|
|
57
|
+
// Save a PHID value (with duplicate prevention)
|
|
58
|
+
const savePhid = useCallback((phid) => {
|
|
59
|
+
if (!hasSaved && phid && phid !== initialPhid) {
|
|
60
|
+
setHasSaved(true);
|
|
61
|
+
onSave(phid);
|
|
62
|
+
}
|
|
63
|
+
}, [hasSaved, initialPhid, onSave]);
|
|
64
|
+
// Handle blur - try to save based on inputText
|
|
65
|
+
const handleBlur = useCallback(() => {
|
|
66
|
+
if (hasSaved)
|
|
67
|
+
return;
|
|
68
|
+
if (inputText) {
|
|
69
|
+
const foundPhid = findPhidByInput(inputText);
|
|
70
|
+
if (foundPhid) {
|
|
71
|
+
savePhid(foundPhid);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}, [hasSaved, inputText, findPhidByInput, savePhid]);
|
|
75
|
+
return (_jsx(PHIDInput, { value: initialPhid, onChange: (newValue) => {
|
|
76
|
+
// onChange is called when user selects from dropdown (click or Enter on highlighted item)
|
|
77
|
+
// The newValue is the PHID. Save immediately if it's a valid known PHID.
|
|
78
|
+
if (isKnownPhid(newValue)) {
|
|
79
|
+
savePhid(newValue);
|
|
80
|
+
}
|
|
81
|
+
}, onInput: (e) => {
|
|
82
|
+
// Track the raw input text for manual entry lookup on blur
|
|
83
|
+
const target = e.target;
|
|
84
|
+
setInputText(target.value);
|
|
85
|
+
}, onBlur: handleBlur, placeholder: "Enter PHID or search by name", className: "w-full", variant: "withValueAndTitle", initialOptions: options, fetchOptionsCallback: fetchOptionsCallback }));
|
|
86
|
+
}
|
|
5
87
|
export function ContributorsSection({ contributors, onAddContributor, onRemoveContributor, }) {
|
|
6
88
|
const drives = useDrives();
|
|
7
89
|
// Map all builder profile FileNodes from all drives with their driveId
|
|
@@ -24,8 +106,8 @@ export function ContributorsSection({ contributors, onAddContributor, onRemoveCo
|
|
|
24
106
|
}, [builderProfileNodesWithDriveId]);
|
|
25
107
|
// Fetch all builder profile documents from all drives
|
|
26
108
|
const builderProfileDocuments = useGetDocuments(builderPhids);
|
|
27
|
-
// Create a map of PHID to document for quick lookup
|
|
28
|
-
const
|
|
109
|
+
// Create a map of PHID to document for quick lookup (local drives)
|
|
110
|
+
const localBuilderProfileMap = useMemo(() => {
|
|
29
111
|
const map = new Map();
|
|
30
112
|
if (!builderProfileDocuments)
|
|
31
113
|
return map;
|
|
@@ -36,10 +118,13 @@ export function ContributorsSection({ contributors, onAddContributor, onRemoveCo
|
|
|
36
118
|
});
|
|
37
119
|
return map;
|
|
38
120
|
}, [builderProfileDocuments]);
|
|
39
|
-
//
|
|
121
|
+
// Fetch remote profiles as fallback for contributors not found locally
|
|
122
|
+
const { profileMap: remoteProfileMap, allProfiles: remoteProfiles } = useRemoteBuilderProfiles(localBuilderProfileMap);
|
|
123
|
+
// Helper function to get builder profile documents from all drives (local + remote)
|
|
40
124
|
const getBuilderProfiles = useCallback(() => {
|
|
41
|
-
|
|
42
|
-
|
|
125
|
+
// Start with local profiles
|
|
126
|
+
const profileOptions = builderProfileNodesWithDriveId.map(({ node }) => {
|
|
127
|
+
const doc = localBuilderProfileMap.get(node.id);
|
|
43
128
|
const name = doc?.state?.global?.name || node.name || node.id;
|
|
44
129
|
return {
|
|
45
130
|
id: node.id,
|
|
@@ -48,18 +133,43 @@ export function ContributorsSection({ contributors, onAddContributor, onRemoveCo
|
|
|
48
133
|
title: name,
|
|
49
134
|
};
|
|
50
135
|
});
|
|
51
|
-
|
|
52
|
-
|
|
136
|
+
// Add remote profiles that aren't already in local
|
|
137
|
+
const localIds = new Set(profileOptions.map((p) => p.id));
|
|
138
|
+
for (const remoteProfile of remoteProfiles) {
|
|
139
|
+
if (!localIds.has(remoteProfile.id)) {
|
|
140
|
+
const name = remoteProfile.state?.name || remoteProfile.id;
|
|
141
|
+
profileOptions.push({
|
|
142
|
+
id: remoteProfile.id,
|
|
143
|
+
label: name,
|
|
144
|
+
value: remoteProfile.id,
|
|
145
|
+
title: name,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return profileOptions;
|
|
150
|
+
}, [builderProfileNodesWithDriveId, localBuilderProfileMap, remoteProfiles]);
|
|
151
|
+
// Helper function to get builder profile data by PHID (local first, then remote fallback)
|
|
53
152
|
const getBuilderProfileByPhid = useCallback((phid) => {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
153
|
+
// Try local first
|
|
154
|
+
const localDoc = localBuilderProfileMap.get(phid);
|
|
155
|
+
if (localDoc) {
|
|
156
|
+
return {
|
|
157
|
+
name: localDoc.state.global?.name || localDoc.header.id,
|
|
158
|
+
slug: localDoc.state.global?.slug || localDoc.header.id,
|
|
159
|
+
icon: localDoc.state.global?.icon || null,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
// Fall back to remote
|
|
163
|
+
const remoteProfile = remoteProfileMap.get(phid);
|
|
164
|
+
if (remoteProfile) {
|
|
165
|
+
return {
|
|
166
|
+
name: remoteProfile.state?.name || remoteProfile.id,
|
|
167
|
+
slug: remoteProfile.state?.slug || remoteProfile.id,
|
|
168
|
+
icon: remoteProfile.state?.icon || null,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
return null;
|
|
172
|
+
}, [localBuilderProfileMap, remoteProfileMap]);
|
|
63
173
|
const contributorData = useMemo(() => {
|
|
64
174
|
return contributors.map((phid) => {
|
|
65
175
|
const profile = getBuilderProfileByPhid(phid);
|
|
@@ -89,35 +199,34 @@ export function ContributorsSection({ contributors, onAddContributor, onRemoveCo
|
|
|
89
199
|
}
|
|
90
200
|
return false;
|
|
91
201
|
},
|
|
92
|
-
renderCellEditor: (
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
const newValue = e.target.value;
|
|
96
|
-
const currentValue = value || "";
|
|
202
|
+
renderCellEditor: (_value, _onChange, context) => {
|
|
203
|
+
const currentPhid = context.row.phid || "";
|
|
204
|
+
const handleSave = (phidValue) => {
|
|
97
205
|
// If a PHID is entered and it's different from current value
|
|
98
|
-
if (
|
|
99
|
-
const existingContributor = contributors.find((contributor) => contributor ===
|
|
206
|
+
if (phidValue && phidValue !== currentPhid) {
|
|
207
|
+
const existingContributor = contributors.find((contributor) => contributor === phidValue);
|
|
100
208
|
if (!existingContributor) {
|
|
101
209
|
// If we're editing an existing row (has an ID), remove the old one first
|
|
102
|
-
if (
|
|
103
|
-
onRemoveContributor(
|
|
210
|
+
if (currentPhid && currentPhid !== phidValue) {
|
|
211
|
+
onRemoveContributor(currentPhid);
|
|
104
212
|
}
|
|
105
213
|
// Add the new contributor
|
|
106
|
-
onAddContributor(
|
|
214
|
+
onAddContributor(phidValue);
|
|
107
215
|
}
|
|
108
216
|
}
|
|
109
|
-
}
|
|
217
|
+
};
|
|
218
|
+
const fetchOptions = (userInput) => {
|
|
110
219
|
const builderProfiles = getBuilderProfiles();
|
|
111
220
|
// Filter profiles based on user input
|
|
112
221
|
if (!userInput.trim()) {
|
|
113
222
|
return Promise.resolve(builderProfiles);
|
|
114
223
|
}
|
|
115
|
-
const filteredProfiles = builderProfiles.filter((profile) => profile.label
|
|
116
|
-
.toLowerCase()
|
|
117
|
-
.includes(userInput.toLowerCase()) ||
|
|
224
|
+
const filteredProfiles = builderProfiles.filter((profile) => profile.label.toLowerCase().includes(userInput.toLowerCase()) ||
|
|
118
225
|
profile.id.toLowerCase().includes(userInput.toLowerCase()));
|
|
119
226
|
return Promise.resolve(filteredProfiles);
|
|
120
|
-
}
|
|
227
|
+
};
|
|
228
|
+
return (_jsx(ContributorPHIDInput, { initialPhid: currentPhid, options: getBuilderProfiles(), onSave: handleSave, fetchOptionsCallback: fetchOptions }, `phid-input-${currentPhid || Date.now()}`));
|
|
229
|
+
},
|
|
121
230
|
renderCell: (value) => {
|
|
122
231
|
if (value === "" || !value) {
|
|
123
232
|
return (_jsx("div", { className: "font-light italic text-gray-500 text-center", children: "+ Double-click to add new contributor (enter or click outside to save)" }));
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export type MarkdownEditorMode = "preview" | "edit" | "live";
|
|
2
|
+
interface MarkdownEditorProps {
|
|
3
|
+
value: string;
|
|
4
|
+
onChange: (value: string) => void;
|
|
5
|
+
onBlur?: (value: string) => void;
|
|
6
|
+
height?: number;
|
|
7
|
+
label?: string;
|
|
8
|
+
labelClassName?: string;
|
|
9
|
+
}
|
|
10
|
+
export declare function MarkdownEditor({ value, onChange, onBlur, height, label, labelClassName, }: MarkdownEditorProps): import("react/jsx-runtime").JSX.Element;
|
|
11
|
+
export {};
|
|
12
|
+
//# sourceMappingURL=markdown-editor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"markdown-editor.d.ts","sourceRoot":"","sources":["../../../../editors/builder-profile/components/markdown-editor.tsx"],"names":[],"mappings":"AAeA,MAAM,MAAM,kBAAkB,GAAG,SAAS,GAAG,MAAM,GAAG,MAAM,CAAC;AAE7D,UAAU,mBAAmB;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACjC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,wBAAgB,cAAc,CAAC,EAC7B,KAAK,EACL,QAAQ,EACR,MAAM,EACN,MAAY,EACZ,KAAiB,EACjB,cAAqD,GACtD,EAAE,mBAAmB,2CA2LrB"}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
import remarkGfm from "remark-gfm";
|
|
4
|
+
import rehypeSlug from "rehype-slug";
|
|
5
|
+
import { useLocalStorage } from "usehooks-ts";
|
|
6
|
+
// Custom preview renderer to make links open in new tabs and ensure proper list rendering
|
|
7
|
+
const previewOptions = {
|
|
8
|
+
components: {
|
|
9
|
+
a: ({ ...props }) => (_jsx("a", { ...props, target: "_blank", rel: "noopener noreferrer" })),
|
|
10
|
+
},
|
|
11
|
+
rehypePlugins: [rehypeSlug],
|
|
12
|
+
remarkPlugins: [remarkGfm],
|
|
13
|
+
};
|
|
14
|
+
export function MarkdownEditor({ value, onChange, onBlur, height = 350, label = "Content", labelClassName = "text-sm leading-4 mb-3 font-medium", }) {
|
|
15
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
16
|
+
const [MDEditor, setMDEditor] = useState(null);
|
|
17
|
+
const [contentValue, setContentValue] = useState(" ");
|
|
18
|
+
const [isLoaded, setIsLoaded] = useState(false);
|
|
19
|
+
const [loadError, setLoadError] = useState(null);
|
|
20
|
+
const [viewMarkdownMode, setViewMarkdownMode] = useLocalStorage("markdown-editor-view-mode", "live");
|
|
21
|
+
// Ensure we have a valid mode for the editor
|
|
22
|
+
const editorMode = viewMarkdownMode || "live";
|
|
23
|
+
// Load the MDEditor component dynamically
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
// Use a more robust dynamic import approach
|
|
26
|
+
const loadEditor = async () => {
|
|
27
|
+
try {
|
|
28
|
+
const module = await import("@uiw/react-md-editor");
|
|
29
|
+
setMDEditor(() => module.default);
|
|
30
|
+
setIsLoaded(true);
|
|
31
|
+
setLoadError(null);
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
console.error("Failed to load MDEditor:", error);
|
|
35
|
+
setLoadError(error instanceof Error ? error.message : "Failed to load editor");
|
|
36
|
+
setIsLoaded(true);
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
// Add a small delay to ensure DOM is ready
|
|
40
|
+
const timer = setTimeout(loadEditor, 0);
|
|
41
|
+
return () => clearTimeout(timer);
|
|
42
|
+
}, []);
|
|
43
|
+
// Update contentValue when value prop changes
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
if (isLoaded) {
|
|
46
|
+
const stringValue = typeof value === "string" ? value : "";
|
|
47
|
+
const safeValue = stringValue.trim() || " ";
|
|
48
|
+
setContentValue(safeValue);
|
|
49
|
+
}
|
|
50
|
+
}, [value, isLoaded]);
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
if (!MDEditor)
|
|
53
|
+
return;
|
|
54
|
+
const handleViewButtonClick = () => {
|
|
55
|
+
const buttonLive = document.querySelector("button[data-name='live']");
|
|
56
|
+
const buttonEdit = document.querySelector("button[data-name='edit']");
|
|
57
|
+
const buttonPreview = document.querySelector("button[data-name='preview']");
|
|
58
|
+
const liveLi = buttonLive?.closest("li");
|
|
59
|
+
const editLi = buttonEdit?.closest("li");
|
|
60
|
+
const previewLi = buttonPreview?.closest("li");
|
|
61
|
+
if (previewLi && previewLi.classList.contains("active")) {
|
|
62
|
+
setViewMarkdownMode("preview");
|
|
63
|
+
}
|
|
64
|
+
if (editLi && editLi.classList.contains("active")) {
|
|
65
|
+
setViewMarkdownMode("edit");
|
|
66
|
+
}
|
|
67
|
+
if (liveLi && liveLi.classList.contains("active")) {
|
|
68
|
+
setViewMarkdownMode("live");
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
document.addEventListener("click", handleViewButtonClick, true);
|
|
72
|
+
return () => {
|
|
73
|
+
document.removeEventListener("click", handleViewButtonClick, true);
|
|
74
|
+
};
|
|
75
|
+
}, [MDEditor, setViewMarkdownMode]);
|
|
76
|
+
// Handle content changes
|
|
77
|
+
const handleContentChange = (newValue) => {
|
|
78
|
+
if (newValue !== undefined) {
|
|
79
|
+
const stringValue = typeof newValue === "string" ? newValue : "";
|
|
80
|
+
// Only replace completely empty strings with a space, preserve all other content
|
|
81
|
+
const safeValue = stringValue === "" ? " " : stringValue;
|
|
82
|
+
setContentValue(safeValue);
|
|
83
|
+
onChange(newValue); // Keep the original value for the parent component
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
// Handle content blur
|
|
87
|
+
const handleContentBlur = (e) => {
|
|
88
|
+
if (onBlur) {
|
|
89
|
+
onBlur(e.target.value);
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
return (_jsxs("div", { className: "w-full", children: [_jsx("style", { children: `
|
|
93
|
+
.w-md-editor-preview ul {
|
|
94
|
+
list-style-type: disc !important;
|
|
95
|
+
padding-left: 2em !important;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.w-md-editor-preview ol {
|
|
99
|
+
list-style-type: decimal !important;
|
|
100
|
+
padding-left: 2em !important;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/* Ensure proper table styling */
|
|
104
|
+
.w-md-editor-preview table {
|
|
105
|
+
border-collapse: collapse;
|
|
106
|
+
width: 100%;
|
|
107
|
+
margin: 1em 0;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.w-md-editor-preview th,
|
|
111
|
+
.w-md-editor-preview td {
|
|
112
|
+
border: 1px solid #ddd;
|
|
113
|
+
padding: 8px;
|
|
114
|
+
text-align: left;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.w-md-editor-preview th {
|
|
118
|
+
background-color: #f5f5f5;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.w-md-editor-text-pre code, .w-md-editor-text-pre div {
|
|
122
|
+
font-size: 16px !important;
|
|
123
|
+
line-height: 24px !important;
|
|
124
|
+
}
|
|
125
|
+
.w-md-editor-text-input {
|
|
126
|
+
font-size: 16px !important;
|
|
127
|
+
line-height: 24px !important;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
` }), label && _jsx("p", { className: labelClassName, children: label }), !isLoaded && (_jsx("div", { className: "w-full border border-gray-300 rounded-md p-3 bg-white", style: { height: `${height}px` }, children: _jsx("div", { className: "w-full h-full flex items-center justify-center text-gray-500", children: "Loading editor..." }) })), isLoaded && loadError && (_jsx("div", { className: "w-full border border-red-300 rounded-md p-3 bg-red-50", style: { height: `${height}px` }, children: _jsxs("div", { className: "w-full h-full flex flex-col items-center justify-center text-red-600", children: [_jsx("p", { className: "text-sm font-medium mb-2", children: "Failed to load markdown editor" }), _jsx("p", { className: "text-xs text-red-500", children: loadError }), _jsx("textarea", { className: "w-full h-full mt-2 p-2 border border-gray-300 rounded text-sm", placeholder: "Fallback text editor - write your content here...", value: value, onChange: (e) => onChange(e.target.value), onBlur: (e) => onBlur?.(e.target.value) })] }) })), isLoaded && MDEditor && (_jsx("div", { "data-color-mode": "light", className: "w-full", children: _jsx(MDEditor, { height: height, value: contentValue, onChange: handleContentChange, onBlur: handleContentBlur, previewOptions: previewOptions, enableScroll: true, preview: editorMode, textareaProps: {
|
|
131
|
+
placeholder: "Write your content here...",
|
|
132
|
+
} }) }))] }));
|
|
133
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"editor.d.ts","sourceRoot":"","sources":["../../../editors/builder-profile/editor.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"editor.d.ts","sourceRoot":"","sources":["../../../editors/builder-profile/editor.tsx"],"names":[],"mappings":"AAwCA,MAAM,CAAC,OAAO,UAAU,MAAM,4CAmgB7B"}
|
|
@@ -12,6 +12,7 @@ import { LinksSection } from "./components/LinksSection.js";
|
|
|
12
12
|
import { ContributorsSection } from "./components/ContributorsSection.js";
|
|
13
13
|
import { ProfilePreview } from "./components/ProfilePreview.js";
|
|
14
14
|
import { ImageUrlInput } from "./components/ImageUrlInput.js";
|
|
15
|
+
import { MarkdownEditor } from "./components/markdown-editor.js";
|
|
15
16
|
const STATUS_OPTIONS = [
|
|
16
17
|
{ value: "ACTIVE", label: "Active", color: "bg-emerald-500" },
|
|
17
18
|
{ value: "INACTIVE", label: "Inactive", color: "bg-slate-400" },
|
|
@@ -242,9 +243,5 @@ export default function Editor() {
|
|
|
242
243
|
}
|
|
243
244
|
}, children: [_jsx("option", { value: "", disabled: true, children: "Select status..." }), STATUS_OPTIONS.map((option) => (_jsx("option", { value: option.value, children: option.label }, option.value)))] })] }), _jsxs("div", { children: [_jsx("label", { className: "field-label", children: "Profile Type" }), _jsxs("div", { className: "type-toggle", children: [_jsx("button", { type: "button", onClick: () => handleTypeChange("INDIVIDUAL"), className: state?.type === "INDIVIDUAL" ? "active" : "", children: _jsxs("span", { className: "flex items-center justify-center gap-2", children: [_jsx(User, { size: 16 }), "Individual"] }) }), _jsx("button", { type: "button", onClick: () => handleTypeChange("TEAM"), className: state?.type === "TEAM" ? "active" : "", children: _jsxs("span", { className: "flex items-center justify-center gap-2", children: [_jsx(Users, { size: 16 }), "Team"] }) })] }), _jsx("p", { className: "field-hint", children: state?.type === "TEAM"
|
|
244
245
|
? "Teams can add contributors to their profile"
|
|
245
|
-
: "Individual profiles represent a single builder" })] })] })] }), _jsxs("div", { className: "section-card p-6", children: [_jsxs("h3", { className: "text-lg font-semibold text-slate-900 mb-6 flex items-center gap-2", children: [_jsx("span", { className: "w-8 h-8 rounded-lg bg-emerald-50 flex items-center justify-center", children: _jsx(FileText, { size: 18, className: "text-emerald-600" }) }), "About"] }), _jsxs("div", { children: [_jsx(
|
|
246
|
-
if (e.target.value !== state?.description) {
|
|
247
|
-
handleFieldChange("description", e.target.value);
|
|
248
|
-
}
|
|
249
|
-
}, placeholder: "Describe your expertise, focus areas, and what you bring to the ecosystem...", rows: 4, autoExpand: true }), _jsx("p", { className: "field-hint", children: "A compelling description helps others understand your capabilities" })] })] }), _jsx(SkillsSection, { skills: state?.skils || [], onAddSkill: handleAddSkill, onRemoveSkill: handleRemoveSkill }), _jsx(ScopesSection, { scopes: state?.scopes || [], onAddScope: handleAddScope, onRemoveScope: handleRemoveScope }), _jsx(LinksSection, { links: state?.links || [], onAddLink: handleAddLink, onEditLink: handleEditLink, onRemoveLink: handleRemoveLink }), state?.type === "TEAM" && (_jsx(ContributorsSection, { contributors: state.contributors, onAddContributor: handleAddContributor, onRemoveContributor: handleRemoveContributor })), _jsx(ToastContainer, { position: "bottom-right" })] })] }));
|
|
246
|
+
: "Individual profiles represent a single builder" })] })] })] }), _jsxs("div", { className: "section-card p-6", children: [_jsxs("h3", { className: "text-lg font-semibold text-slate-900 mb-6 flex items-center gap-2", children: [_jsx("span", { className: "w-8 h-8 rounded-lg bg-emerald-50 flex items-center justify-center", children: _jsx(FileText, { size: 18, className: "text-emerald-600" }) }), "About"] }), _jsxs("div", { children: [_jsx(MarkdownEditor, { label: "What is the builder profile about?", height: 350, value: state?.description || "", onChange: () => { }, onBlur: (value) => handleFieldChange("description", value) }), _jsx("p", { className: "field-hint", children: "A compelling description helps others understand your capabilities" })] })] }), _jsx(SkillsSection, { skills: state?.skils || [], onAddSkill: handleAddSkill, onRemoveSkill: handleRemoveSkill }), _jsx(ScopesSection, { scopes: state?.scopes || [], onAddScope: handleAddScope, onRemoveScope: handleRemoveScope }), _jsx(LinksSection, { links: state?.links || [], onAddLink: handleAddLink, onEditLink: handleEditLink, onRemoveLink: handleRemoveLink }), state?.type === "TEAM" && (_jsx(ContributorsSection, { contributors: state.contributors, onAddContributor: handleAddContributor, onRemoveContributor: handleRemoveContributor })), _jsx(ToastContainer, { position: "bottom-right" })] })] }));
|
|
250
247
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type RemoteBuilderProfile } from "../utils/graphql-client.js";
|
|
2
|
+
interface UseRemoteBuilderProfilesResult {
|
|
3
|
+
/** Map of PHID to remote builder profile data */
|
|
4
|
+
profileMap: Map<string, RemoteBuilderProfile>;
|
|
5
|
+
/** All available remote profiles for selection */
|
|
6
|
+
allProfiles: RemoteBuilderProfile[];
|
|
7
|
+
/** Whether remote data is currently loading */
|
|
8
|
+
isLoading: boolean;
|
|
9
|
+
/** Manually refetch all available profiles */
|
|
10
|
+
refetchAll: () => Promise<void>;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Hook for fetching builder profiles from remote Switchboard drives.
|
|
14
|
+
* Used as a fallback when local drives don't have the builder profile documents.
|
|
15
|
+
*
|
|
16
|
+
* @param localProfileMap - Map of PHIDs that are already resolved locally (to avoid using remote data for those)
|
|
17
|
+
*/
|
|
18
|
+
export declare function useRemoteBuilderProfiles(localProfileMap: Map<string, unknown>): UseRemoteBuilderProfilesResult;
|
|
19
|
+
export {};
|
|
20
|
+
//# sourceMappingURL=useRemoteBuilderProfiles.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useRemoteBuilderProfiles.d.ts","sourceRoot":"","sources":["../../../../editors/builder-profile/hooks/useRemoteBuilderProfiles.ts"],"names":[],"mappings":"AACA,OAAO,EAEL,KAAK,oBAAoB,EAC1B,MAAM,4BAA4B,CAAC;AAEpC,UAAU,8BAA8B;IACtC,iDAAiD;IACjD,UAAU,EAAE,GAAG,CAAC,MAAM,EAAE,oBAAoB,CAAC,CAAC;IAC9C,kDAAkD;IAClD,WAAW,EAAE,oBAAoB,EAAE,CAAC;IACpC,+CAA+C;IAC/C,SAAS,EAAE,OAAO,CAAC;IACnB,8CAA8C;IAC9C,UAAU,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CACjC;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,CACtC,eAAe,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,GACpC,8BAA8B,CA8DhC"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback, useRef } from "react";
|
|
2
|
+
import { fetchAllRemoteBuilderProfiles, } from "../utils/graphql-client.js";
|
|
3
|
+
/**
|
|
4
|
+
* Hook for fetching builder profiles from remote Switchboard drives.
|
|
5
|
+
* Used as a fallback when local drives don't have the builder profile documents.
|
|
6
|
+
*
|
|
7
|
+
* @param localProfileMap - Map of PHIDs that are already resolved locally (to avoid using remote data for those)
|
|
8
|
+
*/
|
|
9
|
+
export function useRemoteBuilderProfiles(localProfileMap) {
|
|
10
|
+
const [profileMap, setProfileMap] = useState(new Map());
|
|
11
|
+
const [allProfiles, setAllProfiles] = useState([]);
|
|
12
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
13
|
+
// Track if we've already started fetching to avoid duplicate requests
|
|
14
|
+
const isFetchingRef = useRef(false);
|
|
15
|
+
const hasFetchedRef = useRef(false);
|
|
16
|
+
// Fetch all available profiles
|
|
17
|
+
const refetchAll = useCallback(async () => {
|
|
18
|
+
// Prevent concurrent fetches
|
|
19
|
+
if (isFetchingRef.current) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
isFetchingRef.current = true;
|
|
23
|
+
setIsLoading(true);
|
|
24
|
+
try {
|
|
25
|
+
const profiles = await fetchAllRemoteBuilderProfiles();
|
|
26
|
+
hasFetchedRef.current = true;
|
|
27
|
+
setAllProfiles(profiles);
|
|
28
|
+
// Build profile map
|
|
29
|
+
const newMap = new Map();
|
|
30
|
+
profiles.forEach((profile) => {
|
|
31
|
+
newMap.set(profile.id, profile);
|
|
32
|
+
});
|
|
33
|
+
setProfileMap(newMap);
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
console.warn("[useRemoteBuilderProfiles] Failed to fetch profiles:", error);
|
|
37
|
+
}
|
|
38
|
+
finally {
|
|
39
|
+
setIsLoading(false);
|
|
40
|
+
isFetchingRef.current = false;
|
|
41
|
+
}
|
|
42
|
+
}, []);
|
|
43
|
+
// Auto-fetch all profiles on mount
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
if (!hasFetchedRef.current && !isFetchingRef.current) {
|
|
46
|
+
void refetchAll();
|
|
47
|
+
}
|
|
48
|
+
}, [refetchAll]);
|
|
49
|
+
// Filter out profiles that exist locally from the returned allProfiles
|
|
50
|
+
const filteredAllProfiles = allProfiles.filter((profile) => !localProfileMap.has(profile.id));
|
|
51
|
+
return {
|
|
52
|
+
profileMap,
|
|
53
|
+
allProfiles: filteredAllProfiles,
|
|
54
|
+
isLoading,
|
|
55
|
+
refetchAll,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GraphQL client utility for fetching remote builder profiles from Switchboard.
|
|
3
|
+
* This is used as a fallback when local drives don't have the builder profile documents.
|
|
4
|
+
*/
|
|
5
|
+
export interface RemoteBuilderProfile {
|
|
6
|
+
id: string;
|
|
7
|
+
state: {
|
|
8
|
+
name: string | null;
|
|
9
|
+
slug: string | null;
|
|
10
|
+
icon: string | null;
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Fetches all available remote drives
|
|
15
|
+
*/
|
|
16
|
+
export declare function fetchRemoteDrives(): Promise<string[]>;
|
|
17
|
+
/**
|
|
18
|
+
* Fetches drive ID by slug
|
|
19
|
+
*/
|
|
20
|
+
export declare function fetchDriveIdBySlug(slug: string): Promise<string | null>;
|
|
21
|
+
/**
|
|
22
|
+
* Fetches all builder profiles from a specific drive
|
|
23
|
+
*/
|
|
24
|
+
export declare function fetchBuilderProfilesFromDrive(driveId: string, options?: {
|
|
25
|
+
silent?: boolean;
|
|
26
|
+
}): Promise<RemoteBuilderProfile[]>;
|
|
27
|
+
/**
|
|
28
|
+
* Fetches a single builder profile by document ID
|
|
29
|
+
*/
|
|
30
|
+
export declare function fetchBuilderProfileById(docId: string, driveId?: string): Promise<RemoteBuilderProfile | null>;
|
|
31
|
+
/**
|
|
32
|
+
* Fetches all builder profiles from all available remote drives.
|
|
33
|
+
* This aggregates profiles from multiple drives into a single list.
|
|
34
|
+
*/
|
|
35
|
+
export declare function fetchAllRemoteBuilderProfiles(): Promise<RemoteBuilderProfile[]>;
|
|
36
|
+
/**
|
|
37
|
+
* Fetches multiple builder profiles by their IDs.
|
|
38
|
+
* Tries to find them across all available remote drives.
|
|
39
|
+
*/
|
|
40
|
+
export declare function fetchRemoteBuilderProfilesByIds(phids: string[]): Promise<Map<string, RemoteBuilderProfile>>;
|
|
41
|
+
//# sourceMappingURL=graphql-client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"graphql-client.d.ts","sourceRoot":"","sources":["../../../../editors/builder-profile/utils/graphql-client.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAuHH,MAAM,WAAW,oBAAoB;IACnC,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE;QACL,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;QACpB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;QACpB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;KACrB,CAAC;CACH;AAcD;;GAEG;AACH,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC,CAG3D;AAED;;GAEG;AACH,wBAAsB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAM7E;AAED;;GAEG;AACH,wBAAsB,6BAA6B,CACjD,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE;IAAE,MAAM,CAAC,EAAE,OAAO,CAAA;CAAE,GAC7B,OAAO,CAAC,oBAAoB,EAAE,CAAC,CAOjC;AAED;;GAEG;AACH,wBAAsB,uBAAuB,CAC3C,KAAK,EAAE,MAAM,EACb,OAAO,CAAC,EAAE,MAAM,GACf,OAAO,CAAC,oBAAoB,GAAG,IAAI,CAAC,CAMtC;AAED;;;GAGG;AACH,wBAAsB,6BAA6B,IAAI,OAAO,CAC5D,oBAAoB,EAAE,CACvB,CA8BA;AAED;;;GAGG;AACH,wBAAsB,+BAA+B,CACnD,KAAK,EAAE,MAAM,EAAE,GACd,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,oBAAoB,CAAC,CAAC,CAiC5C"}
|