mirror of
https://github.com/devonthesofa/obsidian-note-status.git
synced 2026-07-22 05:45:04 +00:00
Merge pull request #91 from devonthesofa/feature/templates-marketplace
Templates marketplace
This commit is contained in:
commit
4001c7bd9f
21 changed files with 1474 additions and 223 deletions
33
README.md
33
README.md
|
|
@ -73,6 +73,39 @@ It works on both Markdown and non-Markdown files, integrates into multiple UI su
|
|||
- 
|
||||
- 
|
||||
|
||||
## Template Marketplace
|
||||
|
||||
You can contribute your own status templates to the plugin!
|
||||
|
||||
### How to contribute (The Easy Way)
|
||||
|
||||
1. Create a custom template in the plugin settings.
|
||||
2. Click the **Share** (📤) icon on your custom template.
|
||||
3. Follow the instructions in the modal to copy the JSON and submit a Pull Request to the [official repository](https://github.com/devonthesofa/obsidian-note-status).
|
||||
4. Maintainers will review your submission and accept it if it passes the revision!
|
||||
|
||||
### How to contribute (Manual)
|
||||
|
||||
1. Fork the repository.
|
||||
...
|
||||
2. Create a new JSON file in the `templates/` folder (e.g., `templates/my-awesome-workflow.json`).
|
||||
3. Follow this format:
|
||||
```json
|
||||
{
|
||||
"id": "my-awesome-workflow",
|
||||
"name": "My Awesome Workflow",
|
||||
"description": "A workflow for doing awesome things",
|
||||
"authorGithub": "your-username",
|
||||
"statuses": [
|
||||
{ "name": "todo", "icon": "📝", "color": "#ff0000", "templateId": "my-awesome-workflow" },
|
||||
{ "name": "done", "icon": "✅", "color": "#00ff00", "templateId": "my-awesome-workflow" }
|
||||
]
|
||||
}
|
||||
```
|
||||
4. Submit a Pull Request with your JSON file!
|
||||
|
||||
Once accepted and merged, your template will be automatically included in the next build and available in the marketplace for all users.
|
||||
|
||||
## Installation
|
||||
|
||||
### Community Plugin Store (recommended)
|
||||
|
|
|
|||
131
components/SettingsUI/MarketplaceBrowseModal.tsx
Normal file
131
components/SettingsUI/MarketplaceBrowseModal.tsx
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
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 {
|
||||
installedTemplates: StatusTemplate[];
|
||||
onInstall: (template: StatusTemplate) => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const MarketplaceBrowseModal: React.FC<MarketplaceBrowseModalProps> = ({
|
||||
installedTemplates,
|
||||
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}
|
||||
onFilterChange={setSearchQuery}
|
||||
placeholder="Search templates..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="template-editor-modal__content">
|
||||
<div className="marketplace-grid">
|
||||
{filteredTemplates.map((template) => {
|
||||
const isInstalled = installedTemplates.some(
|
||||
(t) =>
|
||||
t.name.toLowerCase() ===
|
||||
template.name.toLowerCase(),
|
||||
);
|
||||
return (
|
||||
<div key={template.id} className="marketplace-card">
|
||||
<div className="marketplace-card-header">
|
||||
<div className="marketplace-card-title">
|
||||
{template.name}
|
||||
</div>
|
||||
<div className="marketplace-card-meta">
|
||||
<span className="template-badge badge-info">
|
||||
Marketplace
|
||||
</span>
|
||||
{template.authorGithub && (
|
||||
<div className="marketplace-card-author">
|
||||
by{" "}
|
||||
<a
|
||||
href={`https://github.com/${template.authorGithub}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
onClick={(e) =>
|
||||
e.stopPropagation()
|
||||
}
|
||||
>
|
||||
@{template.authorGithub}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</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>
|
||||
);
|
||||
};
|
||||
107
components/SettingsUI/MarketplaceShareModal.tsx
Normal file
107
components/SettingsUI/MarketplaceShareModal.tsx
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
import React, { useState, useCallback } from "react";
|
||||
import { StatusTemplate } from "@/types/pluginSettings";
|
||||
import { ObsidianIcon } from "../atoms/ObsidianIcon";
|
||||
|
||||
interface MarketplaceShareModalProps {
|
||||
template: StatusTemplate;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const MarketplaceShareModal: React.FC<MarketplaceShareModalProps> = ({
|
||||
template,
|
||||
onClose,
|
||||
}) => {
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
const marketplaceTemplate = {
|
||||
...template,
|
||||
id: template.id.replace(/^custom-/, ""),
|
||||
statuses: template.statuses.map((s) => ({
|
||||
...s,
|
||||
templateId: template.id.replace(/^custom-/, ""),
|
||||
})),
|
||||
};
|
||||
|
||||
const jsonString = JSON.stringify(marketplaceTemplate, null, 2);
|
||||
const filename = `${marketplaceTemplate.id}.json`;
|
||||
|
||||
const handleCopy = useCallback(() => {
|
||||
navigator.clipboard.writeText(jsonString);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
}, [jsonString]);
|
||||
|
||||
return (
|
||||
<div className="template-editor-modal marketplace-share-modal">
|
||||
<div className="template-editor-modal__header">
|
||||
<h2>Submit to Marketplace</h2>
|
||||
</div>
|
||||
|
||||
<div className="template-editor-modal__content">
|
||||
<p>
|
||||
Share your template with the community by following these
|
||||
steps:
|
||||
</p>
|
||||
|
||||
<ol className="marketplace-steps">
|
||||
<li>
|
||||
<strong>Copy</strong> the template JSON data below.
|
||||
</li>
|
||||
<li>
|
||||
Go to the <strong>templates folder</strong> in the
|
||||
official repository:
|
||||
<br />
|
||||
<a
|
||||
href="https://github.com/devonthesofa/obsidian-note-status/tree/master/templates"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
github.com/devonthesofa/obsidian-note-status/templates
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
Click <strong>Add file</strong> >{" "}
|
||||
<strong>Create new file</strong>.
|
||||
</li>
|
||||
<li>
|
||||
Name it <code>{filename}</code> and{" "}
|
||||
<strong>paste</strong> the content.
|
||||
</li>
|
||||
<li>
|
||||
Click <strong>Propose new file</strong> and submit the
|
||||
Pull Request.
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<p className="marketplace-note">
|
||||
<em>
|
||||
Note: Submissions are reviewed by maintainers. Once
|
||||
verified, your template will be included in the next
|
||||
plugin update for all users!
|
||||
</em>
|
||||
</p>
|
||||
|
||||
<div className="marketplace-json-preview">
|
||||
<div className="json-preview-header">
|
||||
<span>{filename}</span>
|
||||
<button
|
||||
className={`copy-btn ${copied ? "success" : ""}`}
|
||||
onClick={handleCopy}
|
||||
>
|
||||
<ObsidianIcon
|
||||
name={copied ? "check" : "copy"}
|
||||
size={14}
|
||||
/>
|
||||
{copied ? "Copied!" : "Copy JSON"}
|
||||
</button>
|
||||
</div>
|
||||
<pre>{jsonString}</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="template-editor-modal__actions">
|
||||
<button onClick={onClose}>Close</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,9 +1,10 @@
|
|||
import React, { useState, useCallback } from "react";
|
||||
import React, { useState, useCallback, useMemo } from "react";
|
||||
import { StatusTemplate } from "@/types/pluginSettings";
|
||||
import { NoteStatus } from "@/types/noteStatus";
|
||||
import { Input } from "@/components/atoms/Input";
|
||||
import { SettingItem } from "./SettingItem";
|
||||
import { CustomStatusItem } from "./CustomStatusItem";
|
||||
import { isCustomTemplate } from "@/utils/templateUtils";
|
||||
|
||||
interface TemplateEditorModalProps {
|
||||
template?: StatusTemplate;
|
||||
|
|
@ -16,8 +17,15 @@ export const TemplateEditorModal: React.FC<TemplateEditorModalProps> = ({
|
|||
onSave,
|
||||
onCancel,
|
||||
}) => {
|
||||
const isCustom = useMemo(
|
||||
() => (template ? isCustomTemplate(template) : true),
|
||||
[template],
|
||||
);
|
||||
const [name, setName] = useState(template?.name || "");
|
||||
const [description, setDescription] = useState(template?.description || "");
|
||||
const [authorGithub, setAuthorGithub] = useState(
|
||||
template?.authorGithub || "",
|
||||
);
|
||||
const [statuses, setStatuses] = useState<NoteStatus[]>(
|
||||
template?.statuses || [
|
||||
{
|
||||
|
|
@ -52,6 +60,7 @@ export const TemplateEditorModal: React.FC<TemplateEditorModalProps> = ({
|
|||
id: templateId,
|
||||
name: name.trim(),
|
||||
description: description.trim(),
|
||||
authorGithub: authorGithub.trim() || undefined,
|
||||
statuses: statusesWithTemplateId,
|
||||
};
|
||||
|
||||
|
|
@ -145,6 +154,23 @@ export const TemplateEditorModal: React.FC<TemplateEditorModalProps> = ({
|
|||
/>
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem
|
||||
name="Author GitHub Username (Optional)"
|
||||
description={
|
||||
isCustom
|
||||
? "Your GitHub username for attribution"
|
||||
: "Original author of the marketplace template"
|
||||
}
|
||||
>
|
||||
<Input
|
||||
variant="text"
|
||||
value={authorGithub}
|
||||
onChange={setAuthorGithub}
|
||||
placeholder="e.g. janedoe"
|
||||
disabled={!isCustom}
|
||||
/>
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem
|
||||
name="Statuses"
|
||||
description="Define the statuses included in this template, including emojis and optional Lucide icons"
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import React from "react";
|
||||
import React, { useMemo } from "react";
|
||||
import { StatusTemplate } from "@/types/pluginSettings";
|
||||
import { StatusDisplay } from "../atoms/StatusDisplay";
|
||||
import { SelectableListItem } from "../atoms/SelectableListItem";
|
||||
import { isCustomTemplate } from "@/utils/templateUtils";
|
||||
import { isCustomTemplate, isTemplateModified } from "@/utils/templateUtils";
|
||||
import { ObsidianIcon } from "../atoms/ObsidianIcon";
|
||||
|
||||
interface TemplateItemProps {
|
||||
|
|
@ -11,6 +11,7 @@ interface TemplateItemProps {
|
|||
onToggle: (templateId: string, enabled: boolean) => void;
|
||||
onEdit?: (template: StatusTemplate) => void;
|
||||
onDelete?: (templateId: string) => void;
|
||||
onShare?: (template: StatusTemplate) => void;
|
||||
}
|
||||
|
||||
export const TemplateItem: React.FC<TemplateItemProps> = ({
|
||||
|
|
@ -19,7 +20,14 @@ export const TemplateItem: React.FC<TemplateItemProps> = ({
|
|||
onToggle,
|
||||
onEdit,
|
||||
onDelete,
|
||||
onShare,
|
||||
}) => {
|
||||
const isCustom = useMemo(() => isCustomTemplate(template), [template]);
|
||||
const isModified = useMemo(
|
||||
() => !isCustom && isTemplateModified(template),
|
||||
[isCustom, template],
|
||||
);
|
||||
|
||||
const handleClick = (e: React.MouseEvent) => {
|
||||
// Don't toggle if clicking action buttons
|
||||
if ((e.target as HTMLElement).closest(".template-item-actions")) {
|
||||
|
|
@ -29,7 +37,7 @@ export const TemplateItem: React.FC<TemplateItemProps> = ({
|
|||
};
|
||||
|
||||
return (
|
||||
<div className={`template-item ${isEnabled ? "enabled" : ""}`}>
|
||||
<div className={`template-item ${isEnabled ? "enabled" : "disabled"}`}>
|
||||
<SelectableListItem
|
||||
selected={isEnabled}
|
||||
onClick={handleClick}
|
||||
|
|
@ -48,14 +56,47 @@ export const TemplateItem: React.FC<TemplateItemProps> = ({
|
|||
<span className="setting-item-name">
|
||||
{template.name}
|
||||
</span>
|
||||
{isCustomTemplate(template.id) && (
|
||||
<span className="template-custom-badge">
|
||||
Custom
|
||||
</span>
|
||||
)}
|
||||
<div className="template-badges">
|
||||
{isEnabled ? (
|
||||
<span className="template-badge badge-success">
|
||||
Active
|
||||
</span>
|
||||
) : (
|
||||
<span className="template-badge badge-muted">
|
||||
Inactive
|
||||
</span>
|
||||
)}
|
||||
{isCustom ? (
|
||||
<span className="template-badge badge-accent">
|
||||
Custom
|
||||
</span>
|
||||
) : (
|
||||
<span className="template-badge badge-info">
|
||||
Marketplace
|
||||
</span>
|
||||
)}
|
||||
{isModified && (
|
||||
<span className="template-badge badge-warning">
|
||||
Modified
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="setting-item-description">
|
||||
{template.description}:
|
||||
{template.description}
|
||||
{template.authorGithub && (
|
||||
<div className="template-author-info">
|
||||
By{" "}
|
||||
<a
|
||||
href={`https://github.com/${template.authorGithub}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
@{template.authorGithub}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="template-statuses">
|
||||
{template.statuses.map((status, index) => (
|
||||
|
|
@ -69,6 +110,18 @@ export const TemplateItem: React.FC<TemplateItemProps> = ({
|
|||
</div>
|
||||
</SelectableListItem>
|
||||
<div className="template-item-actions">
|
||||
{isCustom && onShare && (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onShare(template);
|
||||
}}
|
||||
title="Submit to Marketplace"
|
||||
className="template-marketplace-btn"
|
||||
>
|
||||
<ObsidianIcon name="share" size={16} />
|
||||
</button>
|
||||
)}
|
||||
{onEdit && (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
|
|
|
|||
|
|
@ -2,14 +2,13 @@ import React, { useCallback, useState } from "react";
|
|||
import { PluginSettings, StatusTemplate } from "@/types/pluginSettings";
|
||||
import { TemplateItem } from "./TemplateItem";
|
||||
import { TemplateEditorModal } from "./TemplateEditorModal";
|
||||
import { MarketplaceShareModal } from "./MarketplaceShareModal";
|
||||
import { MarketplaceBrowseModal } from "./MarketplaceBrowseModal";
|
||||
import { ObsidianIcon } from "../atoms/ObsidianIcon";
|
||||
import {
|
||||
generateTemplateId,
|
||||
isTemplateNameUnique,
|
||||
} from "@/utils/templateUtils";
|
||||
import {
|
||||
DEFAULT_ENABLED_TEMPLATES,
|
||||
PREDEFINED_TEMPLATES,
|
||||
} from "@/constants/predefinedTemplates";
|
||||
|
||||
interface TemplateSettingsProps {
|
||||
settings: PluginSettings;
|
||||
|
|
@ -21,9 +20,14 @@ export const TemplateSettings: React.FC<TemplateSettingsProps> = ({
|
|||
onChange,
|
||||
}) => {
|
||||
const [showEditor, setShowEditor] = useState(false);
|
||||
const [showShare, setShowShare] = useState(false);
|
||||
const [showMarketplace, setShowMarketplace] = useState(false);
|
||||
const [editingTemplate, setEditingTemplate] = useState<
|
||||
StatusTemplate | undefined
|
||||
>();
|
||||
const [sharingTemplate, setSharingTemplate] = useState<
|
||||
StatusTemplate | undefined
|
||||
>();
|
||||
|
||||
const handleTemplateToggle = useCallback(
|
||||
(templateId: string, enabled: boolean) => {
|
||||
|
|
@ -52,6 +56,43 @@ export const TemplateSettings: React.FC<TemplateSettingsProps> = ({
|
|||
setShowEditor(true);
|
||||
}, []);
|
||||
|
||||
const handleShareTemplate = useCallback((template: StatusTemplate) => {
|
||||
setSharingTemplate(template);
|
||||
setShowShare(true);
|
||||
}, []);
|
||||
|
||||
const handleInstallTemplate = useCallback(
|
||||
(template: StatusTemplate) => {
|
||||
if (!isTemplateNameUnique(template.name, undefined)) {
|
||||
alert(
|
||||
`A template with the name "${template.name}" is already installed.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Try to use the original ID if it's unique, otherwise generate a new one
|
||||
const isIdUnique = !settings.templates.some(
|
||||
(t) => t.id === template.id,
|
||||
);
|
||||
const newId = isIdUnique
|
||||
? template.id
|
||||
: generateTemplateId(template.name, settings.templates);
|
||||
|
||||
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
|
||||
|
|
@ -72,8 +113,16 @@ export const TemplateSettings: React.FC<TemplateSettingsProps> = ({
|
|||
);
|
||||
onChange("templates", updatedTemplates);
|
||||
} else {
|
||||
// Add new template - generate unique ID
|
||||
const uniqueId = generateTemplateId(template.name);
|
||||
// Add new template
|
||||
// Try to use the original ID if it's unique, otherwise generate a new one
|
||||
const isIdUnique =
|
||||
template.id &&
|
||||
!settings.templates.some((t) => t.id === template.id);
|
||||
|
||||
const uniqueId = isIdUnique
|
||||
? template.id
|
||||
: generateTemplateId(template.name, settings.templates);
|
||||
|
||||
finalTemplate = { ...template, id: uniqueId };
|
||||
|
||||
// Update statuses with the final template ID
|
||||
|
|
@ -118,24 +167,24 @@ 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);
|
||||
}, []);
|
||||
|
||||
const handleCancelShare = useCallback(() => {
|
||||
setShowShare(false);
|
||||
setSharingTemplate(undefined);
|
||||
}, []);
|
||||
|
||||
const handleOpenMarketplace = useCallback(() => {
|
||||
setShowMarketplace(true);
|
||||
}, []);
|
||||
|
||||
const handleCloseMarketplace = useCallback(() => {
|
||||
setShowMarketplace(false);
|
||||
}, []);
|
||||
|
||||
if (showEditor) {
|
||||
return (
|
||||
<div>
|
||||
|
|
@ -149,31 +198,55 @@ export const TemplateSettings: React.FC<TemplateSettingsProps> = ({
|
|||
);
|
||||
}
|
||||
|
||||
if (showShare && sharingTemplate) {
|
||||
return (
|
||||
<div>
|
||||
<h3>Status templates</h3>
|
||||
<MarketplaceShareModal
|
||||
template={sharingTemplate}
|
||||
onClose={handleCancelShare}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (showMarketplace) {
|
||||
return (
|
||||
<div>
|
||||
<h3>Status templates</h3>
|
||||
<MarketplaceBrowseModal
|
||||
installedTemplates={settings.templates}
|
||||
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) => (
|
||||
|
|
@ -186,6 +259,7 @@ export const TemplateSettings: React.FC<TemplateSettingsProps> = ({
|
|||
onToggle={handleTemplateToggle}
|
||||
onEdit={handleEditTemplate}
|
||||
onDelete={handleDeleteTemplate}
|
||||
onShare={handleShareTemplate}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ interface BaseInputProps {
|
|||
style?: React.CSSProperties;
|
||||
onFocus?: () => void;
|
||||
onBlur?: () => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
interface TextInputProps extends BaseInputProps {
|
||||
|
|
@ -38,6 +39,7 @@ export const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|||
style,
|
||||
onFocus,
|
||||
onBlur,
|
||||
disabled,
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
|
|
@ -67,6 +69,7 @@ export const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|||
style={style}
|
||||
onFocus={onFocus}
|
||||
onBlur={onBlur}
|
||||
disabled={disabled}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,8 +1,4 @@
|
|||
import { PluginSettings } from "types/pluginSettings";
|
||||
import {
|
||||
DEFAULT_ENABLED_TEMPLATES,
|
||||
PREDEFINED_TEMPLATES,
|
||||
} from "./predefinedTemplates";
|
||||
|
||||
export const DEFAULT_PLUGIN_SETTINGS: PluginSettings = {
|
||||
fileExplorerIconPosition: "absolute-right",
|
||||
|
|
@ -18,7 +14,7 @@ export const DEFAULT_PLUGIN_SETTINGS: PluginSettings = {
|
|||
showStatusBar: true,
|
||||
autoHideStatusBar: false,
|
||||
enableStatusOverviewPopup: true,
|
||||
templates: [...PREDEFINED_TEMPLATES],
|
||||
templates: [],
|
||||
customStatuses: [],
|
||||
showStatusIconsInExplorer: true,
|
||||
hideUnknownStatusInExplorer: true, // Default to hide unknown status
|
||||
|
|
@ -30,7 +26,7 @@ export const DEFAULT_PLUGIN_SETTINGS: PluginSettings = {
|
|||
enableExperimentalFeatures: true,
|
||||
enableStatusDashboard: true,
|
||||
enableGroupedStatusView: true,
|
||||
enabledTemplates: DEFAULT_ENABLED_TEMPLATES,
|
||||
enabledTemplates: [],
|
||||
useCustomStatusesOnly: false,
|
||||
useMultipleStatuses: true,
|
||||
singleStatusStorageMode: "list",
|
||||
|
|
|
|||
|
|
@ -1,209 +1,278 @@
|
|||
/**
|
||||
* THIS FILE IS AUTOMATICALLY GENERATED. DO NOT EDIT MANUALLY.
|
||||
* To add a new template, create a JSON file in the /templates directory.
|
||||
*/
|
||||
import { StatusTemplate } from "types/pluginSettings";
|
||||
|
||||
/**
|
||||
* Predefined status templates
|
||||
*/
|
||||
export const PREDEFINED_TEMPLATES: StatusTemplate[] = [
|
||||
{
|
||||
id: "colorful",
|
||||
name: "Colorful workflow",
|
||||
description:
|
||||
"A colorful set of workflow statuses with descriptive icons",
|
||||
statuses: [
|
||||
"id": "academic",
|
||||
"name": "Academic research",
|
||||
"description": "Status workflow for academic research and writing",
|
||||
"authorGithub": "soler1212",
|
||||
"statuses": [
|
||||
{
|
||||
name: "idea",
|
||||
icon: "💡",
|
||||
color: "#FFEB3B",
|
||||
templateId: "colorful",
|
||||
"name": "research",
|
||||
"icon": "🔍",
|
||||
"color": "#2196F3",
|
||||
"templateId": "academic"
|
||||
},
|
||||
{
|
||||
name: "draft",
|
||||
icon: "📝",
|
||||
color: "#E0E0E0",
|
||||
templateId: "colorful",
|
||||
"name": "outline",
|
||||
"icon": "📑",
|
||||
"color": "#9E9E9E",
|
||||
"templateId": "academic"
|
||||
},
|
||||
{
|
||||
name: "inProgress",
|
||||
icon: "🔧",
|
||||
color: "#FFC107",
|
||||
templateId: "colorful",
|
||||
"name": "draft",
|
||||
"icon": "✏️",
|
||||
"color": "#FFC107",
|
||||
"templateId": "academic"
|
||||
},
|
||||
{
|
||||
name: "editing",
|
||||
icon: "🖊️",
|
||||
color: "#2196F3",
|
||||
templateId: "colorful",
|
||||
"name": "review",
|
||||
"icon": "🔬",
|
||||
"color": "#9C27B0",
|
||||
"templateId": "academic"
|
||||
},
|
||||
{
|
||||
name: "pending",
|
||||
icon: "⏳",
|
||||
color: "#9C27B0",
|
||||
templateId: "colorful",
|
||||
"name": "revision",
|
||||
"icon": "📝",
|
||||
"color": "#FF5722",
|
||||
"templateId": "academic"
|
||||
},
|
||||
{
|
||||
name: "onHold",
|
||||
icon: "⏸",
|
||||
color: "#9E9E9E",
|
||||
templateId: "colorful",
|
||||
"name": "final",
|
||||
"icon": "📚",
|
||||
"color": "#4CAF50",
|
||||
"templateId": "academic"
|
||||
},
|
||||
{
|
||||
name: "needsUpdate",
|
||||
icon: "🔄",
|
||||
color: "#FF5722",
|
||||
templateId: "colorful",
|
||||
},
|
||||
{
|
||||
name: "completed",
|
||||
icon: "✅",
|
||||
color: "#4CAF50",
|
||||
templateId: "colorful",
|
||||
},
|
||||
{
|
||||
name: "archived",
|
||||
icon: "📦",
|
||||
color: "#795548",
|
||||
templateId: "colorful",
|
||||
},
|
||||
],
|
||||
"name": "published",
|
||||
"icon": "🎓",
|
||||
"color": "#795548",
|
||||
"templateId": "academic"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "minimal",
|
||||
name: "Minimal workflow",
|
||||
description: "A simplified set of essential workflow statuses",
|
||||
statuses: [
|
||||
"id": "colorful",
|
||||
"name": "Colorful workflow",
|
||||
"description": "A colorful set of workflow statuses with descriptive icons",
|
||||
"authorGithub": "soler1212",
|
||||
"statuses": [
|
||||
{
|
||||
name: "todo",
|
||||
icon: "📌",
|
||||
color: "#F44336",
|
||||
templateId: "minimal",
|
||||
"name": "idea",
|
||||
"icon": "💡",
|
||||
"color": "#FFEB3B",
|
||||
"templateId": "colorful"
|
||||
},
|
||||
{
|
||||
name: "inProgress",
|
||||
icon: "⚙️",
|
||||
color: "#2196F3",
|
||||
templateId: "minimal",
|
||||
"name": "draft",
|
||||
"icon": "📝",
|
||||
"color": "#E0E0E0",
|
||||
"templateId": "colorful"
|
||||
},
|
||||
{
|
||||
name: "review",
|
||||
icon: "👀",
|
||||
color: "#9C27B0",
|
||||
templateId: "minimal",
|
||||
"name": "inProgress",
|
||||
"icon": "🔧",
|
||||
"color": "#FFC107",
|
||||
"templateId": "colorful"
|
||||
},
|
||||
{
|
||||
name: "done",
|
||||
icon: "✓",
|
||||
color: "#4CAF50",
|
||||
templateId: "minimal",
|
||||
"name": "editing",
|
||||
"icon": "🖊️",
|
||||
"color": "#2196F3",
|
||||
"templateId": "colorful"
|
||||
},
|
||||
],
|
||||
{
|
||||
"name": "pending",
|
||||
"icon": "⏳",
|
||||
"color": "#9C27B0",
|
||||
"templateId": "colorful"
|
||||
},
|
||||
{
|
||||
"name": "onHold",
|
||||
"icon": "⏸",
|
||||
"color": "#9E9E9E",
|
||||
"templateId": "colorful"
|
||||
},
|
||||
{
|
||||
"name": "needsUpdate",
|
||||
"icon": "🔄",
|
||||
"color": "#FF5722",
|
||||
"templateId": "colorful"
|
||||
},
|
||||
{
|
||||
"name": "completed",
|
||||
"icon": "✅",
|
||||
"color": "#4CAF50",
|
||||
"templateId": "colorful"
|
||||
},
|
||||
{
|
||||
"name": "archived",
|
||||
"icon": "📦",
|
||||
"color": "#795548",
|
||||
"templateId": "colorful"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "academic",
|
||||
name: "Academic research",
|
||||
description: "Status workflow for academic research and writing",
|
||||
statuses: [
|
||||
"id": "creative-writing",
|
||||
"name": "Creative Writing",
|
||||
"description": "Workflow for novelists and creative writers",
|
||||
"authorGithub": "soler1212",
|
||||
"statuses": [
|
||||
{
|
||||
name: "research",
|
||||
icon: "🔍",
|
||||
color: "#2196F3",
|
||||
templateId: "academic",
|
||||
"name": "idea",
|
||||
"icon": "🧠",
|
||||
"color": "#FFD700",
|
||||
"templateId": "creative-writing"
|
||||
},
|
||||
{
|
||||
name: "outline",
|
||||
icon: "📑",
|
||||
color: "#9E9E9E",
|
||||
templateId: "academic",
|
||||
"name": "outline",
|
||||
"icon": "🗺️",
|
||||
"color": "#87CEEB",
|
||||
"templateId": "creative-writing"
|
||||
},
|
||||
{
|
||||
name: "draft",
|
||||
icon: "✏️",
|
||||
color: "#FFC107",
|
||||
templateId: "academic",
|
||||
"name": "first-draft",
|
||||
"icon": "✍️",
|
||||
"color": "#FFA07A",
|
||||
"templateId": "creative-writing"
|
||||
},
|
||||
{
|
||||
name: "review",
|
||||
icon: "🔬",
|
||||
color: "#9C27B0",
|
||||
templateId: "academic",
|
||||
"name": "revision",
|
||||
"icon": "🔍",
|
||||
"color": "#DA70D6",
|
||||
"templateId": "creative-writing"
|
||||
},
|
||||
{
|
||||
name: "revision",
|
||||
icon: "📝",
|
||||
color: "#FF5722",
|
||||
templateId: "academic",
|
||||
},
|
||||
{
|
||||
name: "final",
|
||||
icon: "📚",
|
||||
color: "#4CAF50",
|
||||
templateId: "academic",
|
||||
},
|
||||
{
|
||||
name: "published",
|
||||
icon: "🎓",
|
||||
color: "#795548",
|
||||
templateId: "academic",
|
||||
},
|
||||
],
|
||||
"name": "final-polish",
|
||||
"icon": "✨",
|
||||
"color": "#32CD32",
|
||||
"templateId": "creative-writing"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "project",
|
||||
name: "Project management",
|
||||
description: "Status workflow for project management and tracking",
|
||||
statuses: [
|
||||
"id": "default-starter",
|
||||
"name": "Starter Template",
|
||||
"description": "A simplified set of essential workflow statuses to get you started.",
|
||||
"authorGithub": "soler1212",
|
||||
"statuses": [
|
||||
{
|
||||
name: "planning",
|
||||
icon: "🗓️",
|
||||
color: "#9E9E9E",
|
||||
templateId: "project",
|
||||
"name": "todo",
|
||||
"icon": "📌",
|
||||
"color": "#F44336",
|
||||
"templateId": "default-starter"
|
||||
},
|
||||
{
|
||||
name: "backlog",
|
||||
icon: "📋",
|
||||
color: "#E0E0E0",
|
||||
templateId: "project",
|
||||
"name": "inProgress",
|
||||
"icon": "⚙️",
|
||||
"color": "#2196F3",
|
||||
"templateId": "default-starter"
|
||||
},
|
||||
{
|
||||
name: "ready",
|
||||
icon: "🚦",
|
||||
color: "#8BC34A",
|
||||
templateId: "project",
|
||||
"name": "review",
|
||||
"icon": "👀",
|
||||
"color": "#9C27B0",
|
||||
"templateId": "default-starter"
|
||||
},
|
||||
{
|
||||
name: "inDevelopment",
|
||||
icon: "👨💻",
|
||||
color: "#2196F3",
|
||||
templateId: "project",
|
||||
},
|
||||
{
|
||||
name: "testing",
|
||||
icon: "🧪",
|
||||
color: "#9C27B0",
|
||||
templateId: "project",
|
||||
},
|
||||
{
|
||||
name: "review",
|
||||
icon: "👁️",
|
||||
color: "#FFC107",
|
||||
templateId: "project",
|
||||
},
|
||||
{
|
||||
name: "approved",
|
||||
icon: "👍",
|
||||
color: "#4CAF50",
|
||||
templateId: "project",
|
||||
},
|
||||
{
|
||||
name: "live",
|
||||
icon: "🚀",
|
||||
color: "#3F51B5",
|
||||
templateId: "project",
|
||||
},
|
||||
],
|
||||
"name": "done",
|
||||
"icon": "✓",
|
||||
"color": "#4CAF50",
|
||||
"templateId": "default-starter"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "minimal",
|
||||
"name": "Minimal workflow",
|
||||
"description": "A simplified set of essential workflow statuses",
|
||||
"authorGithub": "soler1212",
|
||||
"statuses": [
|
||||
{
|
||||
"name": "todo",
|
||||
"icon": "📌",
|
||||
"color": "#F44336",
|
||||
"templateId": "minimal"
|
||||
},
|
||||
{
|
||||
"name": "inProgress",
|
||||
"icon": "⚙️",
|
||||
"color": "#2196F3",
|
||||
"templateId": "minimal"
|
||||
},
|
||||
{
|
||||
"name": "review",
|
||||
"icon": "👀",
|
||||
"color": "#9C27B0",
|
||||
"templateId": "minimal"
|
||||
},
|
||||
{
|
||||
"name": "done",
|
||||
"icon": "✓",
|
||||
"color": "#4CAF50",
|
||||
"templateId": "minimal"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "project",
|
||||
"name": "Project management",
|
||||
"description": "Status workflow for project management and tracking",
|
||||
"authorGithub": "soler1212",
|
||||
"statuses": [
|
||||
{
|
||||
"name": "planning",
|
||||
"icon": "🗓️",
|
||||
"color": "#9E9E9E",
|
||||
"templateId": "project"
|
||||
},
|
||||
{
|
||||
"name": "backlog",
|
||||
"icon": "📋",
|
||||
"color": "#E0E0E0",
|
||||
"templateId": "project"
|
||||
},
|
||||
{
|
||||
"name": "ready",
|
||||
"icon": "🚦",
|
||||
"color": "#8BC34A",
|
||||
"templateId": "project"
|
||||
},
|
||||
{
|
||||
"name": "inDevelopment",
|
||||
"icon": "👨💻",
|
||||
"color": "#2196F3",
|
||||
"templateId": "project"
|
||||
},
|
||||
{
|
||||
"name": "testing",
|
||||
"icon": "🧪",
|
||||
"color": "#9C27B0",
|
||||
"templateId": "project"
|
||||
},
|
||||
{
|
||||
"name": "review",
|
||||
"icon": "👁️",
|
||||
"color": "#FFC107",
|
||||
"templateId": "project"
|
||||
},
|
||||
{
|
||||
"name": "approved",
|
||||
"icon": "👍",
|
||||
"color": "#4CAF50",
|
||||
"templateId": "project"
|
||||
},
|
||||
{
|
||||
"name": "live",
|
||||
"icon": "🚀",
|
||||
"color": "#3F51B5",
|
||||
"templateId": "project"
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
/**
|
||||
* Default template IDs that should be enabled by default
|
||||
*/
|
||||
export const DEFAULT_ENABLED_TEMPLATES = ["colorful"];
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { DEFAULT_PLUGIN_SETTINGS } from "@/constants/defaultSettings";
|
||||
import { PluginSettings, SyncGroup } from "@/types/pluginSettings";
|
||||
import { PREDEFINED_TEMPLATES } from "@/constants/predefinedTemplates";
|
||||
import { Plugin, TFile, normalizePath } from "obsidian";
|
||||
import eventBus from "./eventBus";
|
||||
|
||||
|
|
@ -65,7 +66,8 @@ class SettingsService {
|
|||
|
||||
async initialize(plugin: Plugin): Promise<void> {
|
||||
this.plugin = plugin;
|
||||
await this.loadSettings();
|
||||
const loadedData = await this.loadSettings();
|
||||
this.migrateLegacySettings(loadedData);
|
||||
|
||||
if (this.settings.enableExternalStatusSync) {
|
||||
await this.loadFromExternalFile();
|
||||
|
|
@ -104,7 +106,129 @@ class SettingsService {
|
|||
private async loadSettings() {
|
||||
const loadedData = await this.plugin.loadData();
|
||||
this.settings = this.mergeSettings(DEFAULT_PLUGIN_SETTINGS, loadedData);
|
||||
return this.settings;
|
||||
this.deduplicateTemplates();
|
||||
return loadedData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrates legacy settings and ensures new users have a starter template.
|
||||
*/
|
||||
private migrateLegacySettings(loadedData: Partial<PluginSettings> | null) {
|
||||
const hasTemplates =
|
||||
this.settings.templates && this.settings.templates.length > 0;
|
||||
|
||||
// 1. If it's a truly new user (no data at all), give them the starter template
|
||||
if (!loadedData) {
|
||||
this.injectStarterTemplate();
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. If it's an existing user but has no templates, they were using the old defaults
|
||||
if (!hasTemplates) {
|
||||
const legacyIds = [
|
||||
"colorful",
|
||||
"minimal",
|
||||
"academic",
|
||||
"project",
|
||||
"creative-writing",
|
||||
];
|
||||
const enabledLegacy = (this.settings.enabledTemplates || []).filter(
|
||||
(id) => legacyIds.includes(id),
|
||||
);
|
||||
|
||||
if (enabledLegacy.length > 0) {
|
||||
// Restore the ones they had enabled from the marketplace list
|
||||
const toRestore = PREDEFINED_TEMPLATES.filter((t) =>
|
||||
enabledLegacy.includes(t.id),
|
||||
).map((t) => ({
|
||||
...t,
|
||||
id: t.id, // Keep original ID for legacy compatibility
|
||||
statuses: t.statuses.map((s) => ({
|
||||
...s,
|
||||
templateId: t.id,
|
||||
})),
|
||||
}));
|
||||
|
||||
this.settings.templates = toRestore;
|
||||
// Update enabled list
|
||||
this.settings.enabledTemplates = toRestore.map((t) => t.id);
|
||||
this.saveSettings().catch(console.error);
|
||||
} else {
|
||||
// If nothing was enabled, just give them the starter template
|
||||
this.injectStarterTemplate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Injects the default starter template.
|
||||
*/
|
||||
private injectStarterTemplate() {
|
||||
const starterTemplate = {
|
||||
id: "starter",
|
||||
name: "Starter Template",
|
||||
description:
|
||||
"A simplified set of essential workflow statuses to get you started.",
|
||||
authorGithub: "soler1212",
|
||||
statuses: [
|
||||
{
|
||||
name: "todo",
|
||||
icon: "📌",
|
||||
color: "#F44336",
|
||||
templateId: "starter",
|
||||
},
|
||||
{
|
||||
name: "inProgress",
|
||||
icon: "⚙️",
|
||||
color: "#2196F3",
|
||||
templateId: "starter",
|
||||
},
|
||||
{
|
||||
name: "review",
|
||||
icon: "👀",
|
||||
color: "#9C27B0",
|
||||
templateId: "starter",
|
||||
},
|
||||
{
|
||||
name: "done",
|
||||
icon: "✓",
|
||||
color: "#4CAF50",
|
||||
templateId: "starter",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
this.settings.templates = [starterTemplate];
|
||||
if (!this.settings.enabledTemplates.includes(starterTemplate.id)) {
|
||||
this.settings.enabledTemplates.push(starterTemplate.id);
|
||||
}
|
||||
this.saveSettings().catch(console.error);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes duplicate templates with the same name, keeping the first occurrence.
|
||||
*/
|
||||
private deduplicateTemplates() {
|
||||
if (!this.settings.templates) return;
|
||||
|
||||
const seenNames = new Set<string>();
|
||||
const uniqueTemplates = [];
|
||||
let hasDuplicates = false;
|
||||
|
||||
for (const template of this.settings.templates) {
|
||||
const lowerName = template.name.toLowerCase().trim();
|
||||
if (!seenNames.has(lowerName)) {
|
||||
seenNames.add(lowerName);
|
||||
uniqueTemplates.push(template);
|
||||
} else {
|
||||
hasDuplicates = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasDuplicates) {
|
||||
this.settings.templates = uniqueTemplates;
|
||||
this.saveSettings().catch(console.error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import esbuild from "esbuild";
|
||||
import process from "process";
|
||||
import builtins from "builtin-modules";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
const banner =
|
||||
`/*
|
||||
|
|
@ -9,7 +11,45 @@ if you want to view the source, please visit the github repository of this plugi
|
|||
*/
|
||||
`;
|
||||
|
||||
const prod = (process.argv[2] === "production");
|
||||
/**
|
||||
* Automatically generates the predefinedTemplates.ts file by scanning the templates directory.
|
||||
*/
|
||||
const generateTemplatesIndex = () => {
|
||||
const templatesDir = "./templates";
|
||||
const outputFile = "./constants/predefinedTemplates.ts";
|
||||
|
||||
if (!fs.existsSync(templatesDir)) return;
|
||||
|
||||
const files = fs.readdirSync(templatesDir);
|
||||
const templates = [];
|
||||
|
||||
files.forEach((file) => {
|
||||
if (file.endsWith(".json")) {
|
||||
const filePath = path.join(templatesDir, file);
|
||||
const content = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
||||
templates.push(content);
|
||||
}
|
||||
});
|
||||
|
||||
const content = `/**
|
||||
* THIS FILE IS AUTOMATICALLY GENERATED. DO NOT EDIT MANUALLY.
|
||||
* To add a new template, create a JSON file in the /templates directory.
|
||||
*/
|
||||
import { StatusTemplate } from "types/pluginSettings";
|
||||
|
||||
export const PREDEFINED_TEMPLATES: StatusTemplate[] = ${JSON.stringify(templates, null, "\t")};
|
||||
`;
|
||||
|
||||
fs.writeFileSync(outputFile, content);
|
||||
console.log(
|
||||
`\x1b[36m[Templates]\x1b[0m Automatically registered ${templates.length} templates.`,
|
||||
);
|
||||
};
|
||||
|
||||
// Generate templates index before build
|
||||
generateTemplatesIndex();
|
||||
|
||||
const prod = process.argv[2] === "production";
|
||||
|
||||
// CSS bundling function
|
||||
const buildStyles = async () => {
|
||||
|
|
|
|||
|
|
@ -84,12 +84,20 @@
|
|||
box-shadow: 0 2px 8px var(--shadow-color);
|
||||
}
|
||||
|
||||
.template-item.disabled {
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.template-item.enabled {
|
||||
background: var(--background-modifier-hover);
|
||||
border-color: var(--interactive-accent);
|
||||
box-shadow: 0 0 0 1px var(--interactive-accent);
|
||||
}
|
||||
|
||||
.template-item.enabled .setting-item-name {
|
||||
font-weight: var(--font-bold);
|
||||
}
|
||||
|
||||
.template-item.enabled:hover {
|
||||
box-shadow:
|
||||
0 2px 8px var(--shadow-color),
|
||||
|
|
@ -114,20 +122,47 @@
|
|||
margin-bottom: var(--size-2-1);
|
||||
}
|
||||
|
||||
.template-custom-badge {
|
||||
.template-badges {
|
||||
display: flex;
|
||||
gap: var(--size-2-2);
|
||||
}
|
||||
|
||||
.template-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--size-2-1);
|
||||
padding: var(--size-2-1) var(--size-2-2);
|
||||
background: var(--interactive-accent);
|
||||
color: var(--text-on-accent);
|
||||
font-size: var(--font-ui-smaller);
|
||||
font-weight: var(--font-medium);
|
||||
padding: 2px 8px;
|
||||
border-radius: var(--radius-s);
|
||||
font-size: 10px;
|
||||
font-weight: var(--font-bold);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.badge-success {
|
||||
background-color: var(--background-modifier-success);
|
||||
color: var(--text-on-accent);
|
||||
}
|
||||
|
||||
.badge-muted {
|
||||
background-color: var(--background-modifier-border);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.badge-accent {
|
||||
background-color: var(--interactive-accent);
|
||||
color: var(--text-on-accent);
|
||||
}
|
||||
|
||||
.badge-warning {
|
||||
background-color: var(--background-modifier-warning);
|
||||
color: var(--text-on-accent);
|
||||
}
|
||||
|
||||
.badge-info {
|
||||
background-color: var(--background-modifier-info);
|
||||
color: var(--text-on-accent);
|
||||
}
|
||||
|
||||
/* Template Item Actions */
|
||||
.template-item-actions {
|
||||
display: flex;
|
||||
|
|
@ -141,6 +176,31 @@
|
|||
justify-content: center;
|
||||
}
|
||||
|
||||
.template-item-actions button,
|
||||
.template-item-actions a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: var(--size-4-1);
|
||||
border-radius: var(--radius-s);
|
||||
color: var(--text-muted);
|
||||
background: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: all var(--anim-duration-fast) ease;
|
||||
text-decoration: none !important;
|
||||
}
|
||||
|
||||
.template-item-actions button:hover,
|
||||
.template-item-actions a:hover {
|
||||
background: var(--interactive-hover);
|
||||
color: var(--text-normal);
|
||||
}
|
||||
|
||||
.template-marketplace-btn {
|
||||
color: var(--text-accent) !important;
|
||||
}
|
||||
|
||||
/* Template Editor Modal */
|
||||
.template-editor-modal {
|
||||
background: var(--background-primary);
|
||||
|
|
@ -229,6 +289,214 @@
|
|||
transform: none;
|
||||
}
|
||||
|
||||
.template-author-info {
|
||||
display: block;
|
||||
font-size: var(--font-ui-smaller);
|
||||
color: var(--text-muted);
|
||||
margin-top: var(--size-2-1);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.template-author-info a {
|
||||
color: var(--text-accent);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.template-author-info a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Marketplace Share Modal */
|
||||
.marketplace-steps {
|
||||
padding-left: var(--size-4-4);
|
||||
margin-bottom: var(--size-4-4);
|
||||
color: var(--text-normal);
|
||||
}
|
||||
|
||||
.marketplace-steps li {
|
||||
margin-bottom: var(--size-2-2);
|
||||
}
|
||||
|
||||
.marketplace-steps li a {
|
||||
color: var(--text-accent);
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.marketplace-note {
|
||||
margin: var(--size-4-4) 0;
|
||||
padding: var(--size-4-2);
|
||||
background: var(--background-primary-alt);
|
||||
border-left: 4px solid var(--interactive-accent);
|
||||
border-radius: var(--radius-s);
|
||||
font-size: var(--font-ui-small);
|
||||
}
|
||||
|
||||
.marketplace-json-preview {
|
||||
background: var(--background-secondary);
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: var(--radius-m);
|
||||
overflow: hidden;
|
||||
margin-top: var(--size-4-2);
|
||||
}
|
||||
|
||||
.json-preview-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: var(--size-2-2) var(--size-4-2);
|
||||
background: var(--background-primary-alt);
|
||||
border-bottom: 1px solid var(--background-modifier-border);
|
||||
font-family: var(--font-monospace);
|
||||
font-size: var(--font-ui-smaller);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.copy-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--size-2-1);
|
||||
padding: var(--size-2-1) var(--size-2-3) !important;
|
||||
font-size: var(--font-ui-smaller) !important;
|
||||
background: var(--interactive-accent) !important;
|
||||
color: var(--text-on-accent) !important;
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
.copy-btn.success {
|
||||
background: var(--text-success) !important;
|
||||
}
|
||||
|
||||
.marketplace-json-preview pre {
|
||||
margin: 0;
|
||||
padding: var(--size-4-2);
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
font-family: var(--font-monospace);
|
||||
font-size: var(--font-ui-smaller);
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.marketplace-github-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--size-2-2);
|
||||
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-4-2);
|
||||
}
|
||||
|
||||
.marketplace-card-title {
|
||||
font-weight: var(--font-bold);
|
||||
font-size: var(--font-ui-large);
|
||||
color: var(--text-normal);
|
||||
margin-bottom: var(--size-2-2);
|
||||
}
|
||||
|
||||
.marketplace-card-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--size-2-2);
|
||||
}
|
||||
|
||||
.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 {
|
||||
|
|
|
|||
50
templates/academic.json
Normal file
50
templates/academic.json
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
{
|
||||
"id": "academic",
|
||||
"name": "Academic research",
|
||||
"description": "Status workflow for academic research and writing",
|
||||
"authorGithub": "soler1212",
|
||||
"statuses": [
|
||||
{
|
||||
"name": "research",
|
||||
"icon": "🔍",
|
||||
"color": "#2196F3",
|
||||
"templateId": "academic"
|
||||
},
|
||||
{
|
||||
"name": "outline",
|
||||
"icon": "📑",
|
||||
"color": "#9E9E9E",
|
||||
"templateId": "academic"
|
||||
},
|
||||
{
|
||||
"name": "draft",
|
||||
"icon": "✏️",
|
||||
"color": "#FFC107",
|
||||
"templateId": "academic"
|
||||
},
|
||||
{
|
||||
"name": "review",
|
||||
"icon": "🔬",
|
||||
"color": "#9C27B0",
|
||||
"templateId": "academic"
|
||||
},
|
||||
{
|
||||
"name": "revision",
|
||||
"icon": "📝",
|
||||
"color": "#FF5722",
|
||||
"templateId": "academic"
|
||||
},
|
||||
{
|
||||
"name": "final",
|
||||
"icon": "📚",
|
||||
"color": "#4CAF50",
|
||||
"templateId": "academic"
|
||||
},
|
||||
{
|
||||
"name": "published",
|
||||
"icon": "🎓",
|
||||
"color": "#795548",
|
||||
"templateId": "academic"
|
||||
}
|
||||
]
|
||||
}
|
||||
62
templates/colorful.json
Normal file
62
templates/colorful.json
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
{
|
||||
"id": "colorful",
|
||||
"name": "Colorful workflow",
|
||||
"description": "A colorful set of workflow statuses with descriptive icons",
|
||||
"authorGithub": "soler1212",
|
||||
"statuses": [
|
||||
{
|
||||
"name": "idea",
|
||||
"icon": "💡",
|
||||
"color": "#FFEB3B",
|
||||
"templateId": "colorful"
|
||||
},
|
||||
{
|
||||
"name": "draft",
|
||||
"icon": "📝",
|
||||
"color": "#E0E0E0",
|
||||
"templateId": "colorful"
|
||||
},
|
||||
{
|
||||
"name": "inProgress",
|
||||
"icon": "🔧",
|
||||
"color": "#FFC107",
|
||||
"templateId": "colorful"
|
||||
},
|
||||
{
|
||||
"name": "editing",
|
||||
"icon": "🖊️",
|
||||
"color": "#2196F3",
|
||||
"templateId": "colorful"
|
||||
},
|
||||
{
|
||||
"name": "pending",
|
||||
"icon": "⏳",
|
||||
"color": "#9C27B0",
|
||||
"templateId": "colorful"
|
||||
},
|
||||
{
|
||||
"name": "onHold",
|
||||
"icon": "⏸",
|
||||
"color": "#9E9E9E",
|
||||
"templateId": "colorful"
|
||||
},
|
||||
{
|
||||
"name": "needsUpdate",
|
||||
"icon": "🔄",
|
||||
"color": "#FF5722",
|
||||
"templateId": "colorful"
|
||||
},
|
||||
{
|
||||
"name": "completed",
|
||||
"icon": "✅",
|
||||
"color": "#4CAF50",
|
||||
"templateId": "colorful"
|
||||
},
|
||||
{
|
||||
"name": "archived",
|
||||
"icon": "📦",
|
||||
"color": "#795548",
|
||||
"templateId": "colorful"
|
||||
}
|
||||
]
|
||||
}
|
||||
38
templates/creative-writing.json
Normal file
38
templates/creative-writing.json
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
{
|
||||
"id": "creative-writing",
|
||||
"name": "Creative Writing",
|
||||
"description": "Workflow for novelists and creative writers",
|
||||
"authorGithub": "soler1212",
|
||||
"statuses": [
|
||||
{
|
||||
"name": "idea",
|
||||
"icon": "🧠",
|
||||
"color": "#FFD700",
|
||||
"templateId": "creative-writing"
|
||||
},
|
||||
{
|
||||
"name": "outline",
|
||||
"icon": "🗺️",
|
||||
"color": "#87CEEB",
|
||||
"templateId": "creative-writing"
|
||||
},
|
||||
{
|
||||
"name": "first-draft",
|
||||
"icon": "✍️",
|
||||
"color": "#FFA07A",
|
||||
"templateId": "creative-writing"
|
||||
},
|
||||
{
|
||||
"name": "revision",
|
||||
"icon": "🔍",
|
||||
"color": "#DA70D6",
|
||||
"templateId": "creative-writing"
|
||||
},
|
||||
{
|
||||
"name": "final-polish",
|
||||
"icon": "✨",
|
||||
"color": "#32CD32",
|
||||
"templateId": "creative-writing"
|
||||
}
|
||||
]
|
||||
}
|
||||
32
templates/default-starter.json
Normal file
32
templates/default-starter.json
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
{
|
||||
"id": "default-starter",
|
||||
"name": "Starter Template",
|
||||
"description": "A simplified set of essential workflow statuses to get you started.",
|
||||
"authorGithub": "soler1212",
|
||||
"statuses": [
|
||||
{
|
||||
"name": "todo",
|
||||
"icon": "📌",
|
||||
"color": "#F44336",
|
||||
"templateId": "default-starter"
|
||||
},
|
||||
{
|
||||
"name": "inProgress",
|
||||
"icon": "⚙️",
|
||||
"color": "#2196F3",
|
||||
"templateId": "default-starter"
|
||||
},
|
||||
{
|
||||
"name": "review",
|
||||
"icon": "👀",
|
||||
"color": "#9C27B0",
|
||||
"templateId": "default-starter"
|
||||
},
|
||||
{
|
||||
"name": "done",
|
||||
"icon": "✓",
|
||||
"color": "#4CAF50",
|
||||
"templateId": "default-starter"
|
||||
}
|
||||
]
|
||||
}
|
||||
32
templates/minimal.json
Normal file
32
templates/minimal.json
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
{
|
||||
"id": "minimal",
|
||||
"name": "Minimal workflow",
|
||||
"description": "A simplified set of essential workflow statuses",
|
||||
"authorGithub": "soler1212",
|
||||
"statuses": [
|
||||
{
|
||||
"name": "todo",
|
||||
"icon": "📌",
|
||||
"color": "#F44336",
|
||||
"templateId": "minimal"
|
||||
},
|
||||
{
|
||||
"name": "inProgress",
|
||||
"icon": "⚙️",
|
||||
"color": "#2196F3",
|
||||
"templateId": "minimal"
|
||||
},
|
||||
{
|
||||
"name": "review",
|
||||
"icon": "👀",
|
||||
"color": "#9C27B0",
|
||||
"templateId": "minimal"
|
||||
},
|
||||
{
|
||||
"name": "done",
|
||||
"icon": "✓",
|
||||
"color": "#4CAF50",
|
||||
"templateId": "minimal"
|
||||
}
|
||||
]
|
||||
}
|
||||
56
templates/project.json
Normal file
56
templates/project.json
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
{
|
||||
"id": "project",
|
||||
"name": "Project management",
|
||||
"description": "Status workflow for project management and tracking",
|
||||
"authorGithub": "soler1212",
|
||||
"statuses": [
|
||||
{
|
||||
"name": "planning",
|
||||
"icon": "🗓️",
|
||||
"color": "#9E9E9E",
|
||||
"templateId": "project"
|
||||
},
|
||||
{
|
||||
"name": "backlog",
|
||||
"icon": "📋",
|
||||
"color": "#E0E0E0",
|
||||
"templateId": "project"
|
||||
},
|
||||
{
|
||||
"name": "ready",
|
||||
"icon": "🚦",
|
||||
"color": "#8BC34A",
|
||||
"templateId": "project"
|
||||
},
|
||||
{
|
||||
"name": "inDevelopment",
|
||||
"icon": "👨💻",
|
||||
"color": "#2196F3",
|
||||
"templateId": "project"
|
||||
},
|
||||
{
|
||||
"name": "testing",
|
||||
"icon": "🧪",
|
||||
"color": "#9C27B0",
|
||||
"templateId": "project"
|
||||
},
|
||||
{
|
||||
"name": "review",
|
||||
"icon": "👁️",
|
||||
"color": "#FFC107",
|
||||
"templateId": "project"
|
||||
},
|
||||
{
|
||||
"name": "approved",
|
||||
"icon": "👍",
|
||||
"color": "#4CAF50",
|
||||
"templateId": "project"
|
||||
},
|
||||
{
|
||||
"name": "live",
|
||||
"icon": "🚀",
|
||||
"color": "#3F51B5",
|
||||
"templateId": "project"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -26,7 +26,8 @@
|
|||
"lib": ["DOM", "ES5", "ES6", "ES7"],
|
||||
"jsx": "react-jsx",
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"include": ["**/*.ts", "**/*.tsx"]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ export interface StatusTemplate {
|
|||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
authorGithub?: string;
|
||||
statuses: NoteStatus[];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,11 +3,61 @@ import settingsService from "@/core/settingsService";
|
|||
import { PREDEFINED_TEMPLATES } from "@/constants/predefinedTemplates";
|
||||
|
||||
/**
|
||||
* Get all enabled templates
|
||||
* Check if a template is a custom user template (not from marketplace)
|
||||
*/
|
||||
export const isCustomTemplate = (templateId: string): boolean => {
|
||||
const i = PREDEFINED_TEMPLATES.findIndex((t) => t.id === templateId);
|
||||
return i === -1;
|
||||
export const isCustomTemplate = (template: StatusTemplate): boolean => {
|
||||
// Either matches an original marketplace template ID
|
||||
const matchesPredefined = PREDEFINED_TEMPLATES.some(
|
||||
(pt) =>
|
||||
pt.id === template.id ||
|
||||
pt.name.toLowerCase() === template.name.toLowerCase(),
|
||||
);
|
||||
if (matchesPredefined) return false;
|
||||
|
||||
// Or has metadata from marketplace
|
||||
const hasMetadata = !!template.authorGithub;
|
||||
if (hasMetadata) return false;
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if a marketplace template has been modified by the user
|
||||
*/
|
||||
export const isTemplateModified = (template: StatusTemplate): boolean => {
|
||||
const original = PREDEFINED_TEMPLATES.find(
|
||||
(pt) =>
|
||||
pt.id === template.id ||
|
||||
pt.name.toLowerCase() === template.name.toLowerCase(),
|
||||
);
|
||||
|
||||
if (!original) return false;
|
||||
|
||||
// Compare relevant fields to see if anything changed
|
||||
// We stringify for a deep comparison of statuses
|
||||
const originalData = JSON.stringify({
|
||||
name: original.name,
|
||||
description: original.description,
|
||||
statuses: original.statuses.map((s) => ({
|
||||
name: s.name,
|
||||
icon: s.icon,
|
||||
lucideIcon: s.lucideIcon,
|
||||
color: s.color,
|
||||
})),
|
||||
});
|
||||
|
||||
const currentData = JSON.stringify({
|
||||
name: template.name,
|
||||
description: template.description,
|
||||
statuses: template.statuses.map((s) => ({
|
||||
name: s.name,
|
||||
icon: s.icon,
|
||||
lucideIcon: s.lucideIcon,
|
||||
color: s.color,
|
||||
})),
|
||||
});
|
||||
|
||||
return originalData !== currentData;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -36,9 +86,13 @@ export const isTemplateNameUnique = (
|
|||
/**
|
||||
* Generate unique template ID
|
||||
*/
|
||||
export const generateTemplateId = (name: string): string => {
|
||||
export const generateTemplateId = (
|
||||
name: string,
|
||||
existingTemplates?: StatusTemplate[],
|
||||
): string => {
|
||||
const baseId = name.toLowerCase().replace(/[^a-z0-9]/g, "-");
|
||||
const allTemplates = settingsService.settings.templates;
|
||||
const allTemplates =
|
||||
existingTemplates || settingsService.settings.templates;
|
||||
|
||||
let counter = 1;
|
||||
let id = `custom-${baseId}`;
|
||||
|
|
@ -50,3 +104,4 @@ export const generateTemplateId = (name: string): string => {
|
|||
|
||||
return id;
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue