mirror of
https://github.com/devonthesofa/obsidian-note-status.git
synced 2026-07-22 05:45:04 +00:00
feat: implement Template Marketplace gallery with installation workflow
This commit is contained in:
parent
0d5cf5a0d8
commit
e089efd85c
4 changed files with 272 additions and 29 deletions
114
components/SettingsUI/MarketplaceBrowseModal.tsx
Normal file
114
components/SettingsUI/MarketplaceBrowseModal.tsx
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
import React, { useState, useMemo } from "react";
|
||||
import { StatusTemplate } from "@/types/pluginSettings";
|
||||
import { PREDEFINED_TEMPLATES } from "@/constants/predefinedTemplates";
|
||||
import { StatusDisplay } from "../atoms/StatusDisplay";
|
||||
import { ObsidianIcon } from "../atoms/ObsidianIcon";
|
||||
import { SearchFilter } from "../atoms/SearchFilter";
|
||||
|
||||
interface MarketplaceBrowseModalProps {
|
||||
installedIds: string[];
|
||||
onInstall: (template: StatusTemplate) => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const MarketplaceBrowseModal: React.FC<MarketplaceBrowseModalProps> = ({
|
||||
installedIds,
|
||||
onInstall,
|
||||
onClose,
|
||||
}) => {
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
|
||||
const filteredTemplates = useMemo(() => {
|
||||
return PREDEFINED_TEMPLATES.filter(
|
||||
(t) =>
|
||||
t.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
t.description.toLowerCase().includes(searchQuery.toLowerCase()),
|
||||
);
|
||||
}, [searchQuery]);
|
||||
|
||||
return (
|
||||
<div className="template-editor-modal marketplace-browse-modal">
|
||||
<div className="template-editor-modal__header">
|
||||
<div className="marketplace-header-content">
|
||||
<h2>Template Marketplace</h2>
|
||||
<SearchFilter
|
||||
value={searchQuery}
|
||||
onChange={setSearchQuery}
|
||||
placeholder="Search templates..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="template-editor-modal__content">
|
||||
<div className="marketplace-grid">
|
||||
{filteredTemplates.map((template) => {
|
||||
const isInstalled =
|
||||
installedIds.includes(template.id) ||
|
||||
installedIds.includes(`custom-${template.id}`);
|
||||
return (
|
||||
<div key={template.id} className="marketplace-card">
|
||||
<div className="marketplace-card-header">
|
||||
<div className="marketplace-card-title">
|
||||
{template.name}
|
||||
</div>
|
||||
{template.author && (
|
||||
<div className="marketplace-card-author">
|
||||
by {template.author}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="marketplace-card-description">
|
||||
{template.description}
|
||||
</div>
|
||||
<div className="marketplace-card-statuses">
|
||||
{template.statuses.map((status, idx) => (
|
||||
<StatusDisplay
|
||||
key={idx}
|
||||
status={status}
|
||||
variant="template"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="marketplace-card-actions">
|
||||
<button
|
||||
className={`marketplace-install-btn ${
|
||||
isInstalled
|
||||
? "installed"
|
||||
: "mod-cta"
|
||||
}`}
|
||||
onClick={() =>
|
||||
!isInstalled && onInstall(template)
|
||||
}
|
||||
disabled={isInstalled}
|
||||
>
|
||||
<ObsidianIcon
|
||||
name={
|
||||
isInstalled
|
||||
? "check"
|
||||
: "download"
|
||||
}
|
||||
size={16}
|
||||
/>
|
||||
{isInstalled
|
||||
? "Installed"
|
||||
: "Install Template"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{filteredTemplates.length === 0 && (
|
||||
<div className="marketplace-empty">
|
||||
<ObsidianIcon name="search-slash" size={48} />
|
||||
<p>No templates found matching your search.</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="template-editor-modal__actions">
|
||||
<button onClick={onClose}>Close</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -3,14 +3,11 @@ import { PluginSettings, StatusTemplate } from "@/types/pluginSettings";
|
|||
import { TemplateItem } from "./TemplateItem";
|
||||
import { TemplateEditorModal } from "./TemplateEditorModal";
|
||||
import { MarketplaceShareModal } from "./MarketplaceShareModal";
|
||||
import { MarketplaceBrowseModal } from "./MarketplaceBrowseModal";
|
||||
import {
|
||||
generateTemplateId,
|
||||
isTemplateNameUnique,
|
||||
} from "@/utils/templateUtils";
|
||||
import {
|
||||
DEFAULT_ENABLED_TEMPLATES,
|
||||
PREDEFINED_TEMPLATES,
|
||||
} from "@/constants/predefinedTemplates";
|
||||
|
||||
interface TemplateSettingsProps {
|
||||
settings: PluginSettings;
|
||||
|
|
@ -23,6 +20,7 @@ export const TemplateSettings: React.FC<TemplateSettingsProps> = ({
|
|||
}) => {
|
||||
const [showEditor, setShowEditor] = useState(false);
|
||||
const [showShare, setShowShare] = useState(false);
|
||||
const [showMarketplace, setShowMarketplace] = useState(false);
|
||||
const [editingTemplate, setEditingTemplate] = useState<
|
||||
StatusTemplate | undefined
|
||||
>();
|
||||
|
|
@ -62,6 +60,24 @@ export const TemplateSettings: React.FC<TemplateSettingsProps> = ({
|
|||
setShowShare(true);
|
||||
}, []);
|
||||
|
||||
const handleInstallTemplate = useCallback(
|
||||
(template: StatusTemplate) => {
|
||||
const newId = generateTemplateId(template.name);
|
||||
const installedTemplate: StatusTemplate = {
|
||||
...template,
|
||||
id: newId,
|
||||
statuses: template.statuses.map((s) => ({
|
||||
...s,
|
||||
templateId: newId,
|
||||
})),
|
||||
};
|
||||
|
||||
onChange("templates", [...settings.templates, installedTemplate]);
|
||||
handleTemplateToggle(newId, true);
|
||||
},
|
||||
[settings.templates, onChange, handleTemplateToggle],
|
||||
);
|
||||
|
||||
const handleSaveTemplate = useCallback(
|
||||
(template: StatusTemplate) => {
|
||||
// Validate name uniqueness
|
||||
|
|
@ -128,19 +144,6 @@ export const TemplateSettings: React.FC<TemplateSettingsProps> = ({
|
|||
[settings.templates, settings.enabledTemplates, onChange],
|
||||
);
|
||||
|
||||
const handleResetToDefaults = useCallback(() => {
|
||||
const confirmed = confirm(
|
||||
"Reset to default templates? This will:\n" +
|
||||
"• Remove all custom templates\n" +
|
||||
"• Reset enabled templates to defaults\n" +
|
||||
"This action cannot be undone.",
|
||||
);
|
||||
if (!confirmed) return;
|
||||
|
||||
onChange("templates", [...PREDEFINED_TEMPLATES]);
|
||||
onChange("enabledTemplates", [...DEFAULT_ENABLED_TEMPLATES]);
|
||||
}, [onChange]);
|
||||
|
||||
const handleCancelEditor = useCallback(() => {
|
||||
setShowEditor(false);
|
||||
setEditingTemplate(undefined);
|
||||
|
|
@ -151,6 +154,14 @@ export const TemplateSettings: React.FC<TemplateSettingsProps> = ({
|
|||
setSharingTemplate(undefined);
|
||||
}, []);
|
||||
|
||||
const handleOpenMarketplace = useCallback(() => {
|
||||
setShowMarketplace(true);
|
||||
}, []);
|
||||
|
||||
const handleCloseMarketplace = useCallback(() => {
|
||||
setShowMarketplace(false);
|
||||
}, []);
|
||||
|
||||
if (showEditor) {
|
||||
return (
|
||||
<div>
|
||||
|
|
@ -176,31 +187,43 @@ export const TemplateSettings: React.FC<TemplateSettingsProps> = ({
|
|||
);
|
||||
}
|
||||
|
||||
if (showMarketplace) {
|
||||
return (
|
||||
<div>
|
||||
<h3>Status templates</h3>
|
||||
<MarketplaceBrowseModal
|
||||
installedIds={settings.templates.map((t) => t.id)}
|
||||
onInstall={handleInstallTemplate}
|
||||
onClose={handleCloseMarketplace}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h3>Status templates</h3>
|
||||
<p>
|
||||
Enable predefined templates to quickly add common status
|
||||
workflows, or create your own custom templates.
|
||||
Browse the marketplace to find common status workflows or create
|
||||
your own custom templates.
|
||||
</p>
|
||||
|
||||
{/* Custom Templates Section */}
|
||||
<div className="template-section">
|
||||
<h4 className="template-section-title">Custom Templates</h4>
|
||||
|
||||
<div className="template-settings-actions">
|
||||
<button
|
||||
className="mod-cta template-create-btn"
|
||||
className="mod-cta marketplace-browse-btn"
|
||||
onClick={handleOpenMarketplace}
|
||||
>
|
||||
<ObsidianIcon name="globe" size={16} />
|
||||
Browse Marketplace
|
||||
</button>
|
||||
<button
|
||||
className="template-create-btn"
|
||||
onClick={handleCreateTemplate}
|
||||
>
|
||||
+ Create Template
|
||||
</button>
|
||||
<button
|
||||
className="template-reset-btn"
|
||||
onClick={handleResetToDefaults}
|
||||
>
|
||||
🔄 Reset to Defaults
|
||||
</button>
|
||||
</div>
|
||||
<div className="template-list">
|
||||
{(settings.templates || []).map((template) => (
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -349,6 +349,112 @@
|
|||
text-decoration: none !important;
|
||||
}
|
||||
|
||||
/* Marketplace Browse Modal */
|
||||
.marketplace-header-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
gap: var(--size-4-4);
|
||||
}
|
||||
|
||||
.marketplace-header-content h2 {
|
||||
margin: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.marketplace-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: var(--size-4-4);
|
||||
padding: var(--size-2-2);
|
||||
}
|
||||
|
||||
.marketplace-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--background-primary-alt);
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: var(--radius-m);
|
||||
padding: var(--size-4-4);
|
||||
transition: all var(--anim-duration-fast) ease;
|
||||
}
|
||||
|
||||
.marketplace-card:hover {
|
||||
border-color: var(--interactive-accent);
|
||||
box-shadow: var(--shadow-s);
|
||||
}
|
||||
|
||||
.marketplace-card-header {
|
||||
margin-bottom: var(--size-2-2);
|
||||
}
|
||||
|
||||
.marketplace-card-title {
|
||||
font-weight: var(--font-bold);
|
||||
font-size: var(--font-ui-medium);
|
||||
color: var(--text-normal);
|
||||
}
|
||||
|
||||
.marketplace-card-author {
|
||||
font-size: var(--font-ui-smaller);
|
||||
color: var(--text-muted);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.marketplace-card-description {
|
||||
font-size: var(--font-ui-small);
|
||||
color: var(--text-muted);
|
||||
margin-bottom: var(--size-4-2);
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.marketplace-card-statuses {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--size-2-1);
|
||||
margin-bottom: var(--size-4-4);
|
||||
}
|
||||
|
||||
.marketplace-card-actions {
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.marketplace-install-btn {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--size-2-2);
|
||||
}
|
||||
|
||||
.marketplace-install-btn.installed {
|
||||
background: var(--background-modifier-success-accent) !important;
|
||||
color: var(--text-on-accent) !important;
|
||||
opacity: 0.8;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.marketplace-empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: var(--size-4-12);
|
||||
color: var(--text-muted);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.marketplace-empty p {
|
||||
margin-top: var(--size-4-2);
|
||||
font-size: var(--font-ui-large);
|
||||
}
|
||||
|
||||
.marketplace-browse-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--size-2-2);
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 768px) {
|
||||
.template-settings-actions {
|
||||
|
|
|
|||
Loading…
Reference in a new issue