mirror of
https://github.com/wahyd4/links.git
synced 2026-08-08 21:04:53 +10:00
refactor: merge Template back into Link type (per user decision)
- Revert TEMPLATE as separate LinkType; templates are plain links whose
URL contains {param} placeholders
- migration 0055: data-migrate any TEMPLATE links back to LINK, roll back
choices (0054 stays in history)
- link_form: back to 3 tabs (Link/Custom/Action); Link tab owns the URL
field plus a dashed '🔧 Add template parameters' toggle; the Sketch
builder auto-shows when the URL contains { } (and stays visible after a
manual toggle); hidden on Custom/Action tabs
- badges (row/detail/search) detect templates by URL content again;
detail page shows Original URL for plain links
- views/forms: drop is_template plumbing and TEMPLATE validation
This commit is contained in:
@@ -130,8 +130,6 @@ class LinkForm(forms.ModelForm):
|
||||
|
||||
if link_type == Link.LinkType.LINK and not original_url:
|
||||
raise forms.ValidationError(_("Original URL is required for Link type."))
|
||||
elif link_type == Link.LinkType.TEMPLATE and not original_url:
|
||||
raise forms.ValidationError(_("Original URL is required for Template type."))
|
||||
elif link_type == Link.LinkType.CUSTOM and not text:
|
||||
raise forms.ValidationError(_("Text is required for Custom type."))
|
||||
elif link_type == Link.LinkType.ACTION:
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
# Generated by Django 5.2.12 on 2026-08-02 08:17
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
def template_to_link(apps, schema_editor):
|
||||
"""Template links were merged back into plain links."""
|
||||
Link = apps.get_model('links', 'Link')
|
||||
Link.objects.filter(link_type='TEMPLATE').update(link_type='LINK')
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('links', '0054_add_template_link_type'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(template_to_link, migrations.RunPython.noop),
|
||||
migrations.AlterField(
|
||||
model_name='link',
|
||||
name='link_type',
|
||||
field=models.CharField(choices=[('LINK', 'Link'), ('CUSTOM', 'Custom'), ('ACTION', 'Action')], default='LINK', max_length=10),
|
||||
),
|
||||
]
|
||||
@@ -19,7 +19,6 @@ logger = logging.getLogger(__name__)
|
||||
class Link(models.Model):
|
||||
class LinkType(models.TextChoices):
|
||||
LINK = 'LINK', _('Link')
|
||||
TEMPLATE = 'TEMPLATE', _('Template')
|
||||
CUSTOM = 'CUSTOM', _('Custom')
|
||||
ACTION = 'ACTION', _('Action')
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<a href="{% url 'redirect_to_original' link.alias %}" target="_blank" rel="noopener noreferrer" class="alias-link">{{ link.alias }}</a>
|
||||
</td>
|
||||
<td class="hidden sm:table-cell">
|
||||
{% if link.link_type == 'TEMPLATE' or '{' in link.original_url|default:'' and '}' in link.original_url|default:'' %}
|
||||
{% if '{' in link.original_url|default:'' and '}' in link.original_url|default:'' %}
|
||||
<span class="badge badge-template"><i class="fas fa-magic" aria-hidden="true"></i>{% trans "Template" %}</span>
|
||||
{% elif link.link_type == 'LINK' %}
|
||||
<span class="badge badge-link"><i class="fas fa-link" aria-hidden="true"></i>{% trans "Link" %}</span>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<div class="link-header-row">
|
||||
<div class="link-type-badge-row">
|
||||
<h1 class="link-detail-title">{% trans "Link Details" %}</h1>
|
||||
{% if link.link_type == 'TEMPLATE' or '{' in link.original_url|default:'' and '}' in link.original_url|default:'' %}
|
||||
{% if '{' in link.original_url|default:'' and '}' in link.original_url|default:'' %}
|
||||
<span class="badge badge-template"><i class="fas fa-magic" aria-hidden="true"></i>{% trans "Template" %}</span>
|
||||
{% elif link.link_type == 'LINK' %}
|
||||
<span class="badge badge-link"><i class="fas fa-link" aria-hidden="true"></i>{% trans "Link" %}</span>
|
||||
@@ -53,7 +53,7 @@
|
||||
<dt>{% trans "Updated At" %}</dt>
|
||||
<dd>{{ link.updated_at|date:"Y-m-d H:i" }}</dd>
|
||||
</div>
|
||||
{% if link.link_type == 'LINK' or link.link_type == 'TEMPLATE' %}
|
||||
{% if link.link_type == 'LINK' %}
|
||||
<div class="link-meta-item span-2">
|
||||
<dt>{% trans "Original URL" %}</dt>
|
||||
<dd>
|
||||
|
||||
@@ -79,6 +79,15 @@
|
||||
padding: .9rem 1rem;
|
||||
}
|
||||
.tpl-label { font-size: .7rem; font-weight: 600; color: #7c6ba6; text-transform: uppercase; letter-spacing: .06em; }
|
||||
.tpl-toggle {
|
||||
display: inline-flex; align-items: center; gap: .35rem;
|
||||
margin-top: .6rem; border: 1px dashed #c4b5fd; background: #faf9ff;
|
||||
color: #6d28d9; border-radius: 10px;
|
||||
padding: .45rem 1rem; font-size: .85rem; font-weight: 600;
|
||||
cursor: pointer; transition: all .15s;
|
||||
}
|
||||
.tpl-toggle:hover { background: #ede9fe; border-color: #a78bfa; }
|
||||
.tpl-toggle.open { border-style: solid; background: #ede9fe; }
|
||||
.tpl-hint { font-size: .72rem; color: #9ca3af; margin-top: .2rem; line-height: 1.4; }
|
||||
.tpl-chip {
|
||||
display: inline-flex; align-items: center; gap: .35rem;
|
||||
@@ -154,12 +163,9 @@
|
||||
<!-- Link Type Tabs -->
|
||||
<div class="mb-4">
|
||||
<div class="flex border-b border-gray-200">
|
||||
<button type="button" class="py-2 px-4 text-sm font-medium text-center {% if not is_custom and not is_template and not is_action %}text-blue-600 border-b-2 border-blue-600{% else %}text-gray-500 border-b-2 border-transparent{% endif %} hover:text-blue-700 hover:border-blue-700 focus:outline-none" id="link-tab">
|
||||
<button type="button" class="py-2 px-4 text-sm font-medium text-center {% if not is_custom and not is_action %}text-blue-600 border-b-2 border-blue-600{% else %}text-gray-500 border-b-2 border-transparent{% endif %} hover:text-blue-700 hover:border-blue-700 focus:outline-none" id="link-tab">
|
||||
{% trans "Link" %}
|
||||
</button>
|
||||
<button type="button" class="py-2 px-4 text-sm font-medium text-center {% if is_template %}text-purple-600 border-b-2 border-purple-600{% else %}text-gray-500 border-b-2 border-transparent{% endif %} hover:text-purple-700 hover:border-purple-300 focus:outline-none" id="template-tab">
|
||||
🔧 {% trans "Template" %}
|
||||
</button>
|
||||
<button type="button" class="py-2 px-4 text-sm font-medium text-center {% if is_custom %}text-green-600 border-b-2 border-green-600{% else %}text-gray-500 border-b-2 border-transparent{% endif %} hover:text-green-700 hover:border-green-300 focus:outline-none" id="custom-tab">
|
||||
{% trans "Custom" %}
|
||||
</button>
|
||||
@@ -170,17 +176,12 @@
|
||||
</div>
|
||||
|
||||
<!-- Link Type Description -->
|
||||
<div id="link-description" class="bg-blue-50 border-l-4 border-blue-400 p-4 mb-4 {% if is_custom or is_template or is_action %}hidden{% endif %}">
|
||||
<div id="link-description" class="bg-blue-50 border-l-4 border-blue-400 p-4 mb-4 {% if is_custom or is_action %}hidden{% endif %}">
|
||||
<p class="text-sm text-blue-700">
|
||||
{% trans "Link type is used for redirecting to an external URL. Use this when you want to create a short alias for a long URL." %}
|
||||
</p>
|
||||
</div>
|
||||
<div id="template-description" class="bg-purple-50 border-l-4 border-purple-400 p-4 mb-4 {% if not is_template %}hidden{% endif %}">
|
||||
<p class="text-sm text-purple-700">
|
||||
{% trans "Template type creates dynamic URLs with parameters. Click a parameter chip below to insert it, or type {name,default=value} in the URL." %}
|
||||
</p>
|
||||
<p class="text-sm text-purple-700 mt-2">
|
||||
{% trans "Example: https://google.com/search?q={q, default=hello} — opening go/alias?q=weather fills the parameter." %}
|
||||
<p class="text-sm text-blue-700 mt-2">
|
||||
{% trans "For dynamic URLs with parameters, click “Add template parameters” below the URL field." %}
|
||||
</p>
|
||||
</div>
|
||||
<div id="custom-description" class="bg-green-50 border-l-4 border-green-400 p-4 mb-4 {% if not is_custom %}hidden{% endif %}">
|
||||
@@ -227,8 +228,9 @@
|
||||
value="{{ field.value|default_if_none:'' }}"
|
||||
class="shadow appearance-none rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
|
||||
{% if field.field.required %}required{% endif %}>
|
||||
<!-- Sketch-style template builder: Template type only -->
|
||||
<div id="template-builder" class="template-builder {% if not is_template %}hidden{% endif %}">
|
||||
<!-- Sketch-style template builder (Link type; auto-shows when the URL has {params}) -->
|
||||
<button type="button" id="tpl-toggle" class="tpl-toggle">🔧 <span id="tpl-toggle-label">{% trans "Add template parameters" %}</span></button>
|
||||
<div id="template-builder" class="template-builder hidden">
|
||||
<div class="tpl-label">🔧 {% trans "Template parameters" %}</div>
|
||||
<div class="tpl-hint">{% trans "Click a parameter to insert it into the URL. The text after go/alias/… fills the parameter when the link is opened." %}</div>
|
||||
<div class="tpl-chips" id="tpl-chips">
|
||||
@@ -361,7 +363,6 @@
|
||||
});
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const linkTab = document.getElementById('link-tab');
|
||||
const templateTab = document.getElementById('template-tab');
|
||||
const customTab = document.getElementById('custom-tab');
|
||||
const actionTab = document.getElementById('action-tab');
|
||||
const linkFields = document.querySelectorAll('.link-field');
|
||||
@@ -369,15 +370,18 @@
|
||||
const actionFields = document.querySelectorAll('.action-field');
|
||||
const linkDescription = document.getElementById('link-description');
|
||||
const customDescription = document.getElementById('custom-description');
|
||||
const templateDescription = document.getElementById('template-description');
|
||||
const actionDescription = document.getElementById('action-description');
|
||||
const linkTypeInput = document.getElementById('id_link_type');
|
||||
const textField = document.querySelector('.custom-field');
|
||||
const tplBuilder = document.getElementById('template-builder');
|
||||
const tplToggle = document.getElementById('tpl-toggle');
|
||||
const tplToggleLabel = document.getElementById('tpl-toggle-label');
|
||||
const urlInput = document.getElementById('id_original_url');
|
||||
let simplemde = null;
|
||||
let builderManuallyOpen = false;
|
||||
|
||||
function setTabActive(tab, color) {
|
||||
[linkTab, templateTab, customTab, actionTab].forEach(function (t) {
|
||||
[linkTab, customTab, actionTab].forEach(function (t) {
|
||||
t.classList.remove('text-blue-600', 'border-blue-600',
|
||||
'text-purple-600', 'border-purple-600',
|
||||
'text-green-600', 'border-green-600');
|
||||
@@ -393,6 +397,17 @@
|
||||
if (label) label.classList.toggle('hidden', !visible);
|
||||
});
|
||||
}
|
||||
function syncBuilderVisibility() {
|
||||
if (!tplBuilder || !tplToggle) return;
|
||||
const url = urlInput ? urlInput.value : '';
|
||||
const hasParams = url.indexOf('{') > -1 && url.indexOf('}') > -1;
|
||||
const show = hasParams || builderManuallyOpen;
|
||||
tplBuilder.classList.toggle('hidden', !show);
|
||||
tplToggle.classList.toggle('open', show);
|
||||
if (tplToggleLabel) {
|
||||
tplToggleLabel.textContent = show ? '{% trans "Hide template parameters" %}' : '{% trans "Add template parameters" %}';
|
||||
}
|
||||
}
|
||||
|
||||
function showLinkFields() {
|
||||
setTabActive(linkTab, 'blue');
|
||||
@@ -400,26 +415,12 @@
|
||||
showFieldSet(customFields, false);
|
||||
showFieldSet(actionFields, false);
|
||||
linkDescription.classList.remove('hidden');
|
||||
templateDescription.classList.add('hidden');
|
||||
customDescription.classList.add('hidden');
|
||||
actionDescription.classList.add('hidden');
|
||||
linkTypeInput.value = 'LINK';
|
||||
textField.classList.add('hidden');
|
||||
if (tplBuilder) tplBuilder.classList.add('hidden');
|
||||
}
|
||||
|
||||
function showTemplateFields() {
|
||||
setTabActive(templateTab, 'purple');
|
||||
showFieldSet(linkFields, true);
|
||||
showFieldSet(customFields, false);
|
||||
showFieldSet(actionFields, false);
|
||||
linkDescription.classList.add('hidden');
|
||||
templateDescription.classList.remove('hidden');
|
||||
customDescription.classList.add('hidden');
|
||||
actionDescription.classList.add('hidden');
|
||||
linkTypeInput.value = 'TEMPLATE';
|
||||
textField.classList.add('hidden');
|
||||
if (tplBuilder) tplBuilder.classList.remove('hidden');
|
||||
if (tplToggle) tplToggle.classList.remove('hidden');
|
||||
syncBuilderVisibility();
|
||||
}
|
||||
|
||||
function showCustomFields() {
|
||||
@@ -428,12 +429,12 @@
|
||||
showFieldSet(customFields, true);
|
||||
showFieldSet(actionFields, false);
|
||||
customDescription.classList.remove('hidden');
|
||||
templateDescription.classList.add('hidden');
|
||||
linkDescription.classList.add('hidden');
|
||||
actionDescription.classList.add('hidden');
|
||||
linkTypeInput.value = 'CUSTOM';
|
||||
textField.classList.remove('hidden');
|
||||
if (tplBuilder) tplBuilder.classList.add('hidden');
|
||||
if (tplToggle) tplToggle.classList.add('hidden');
|
||||
initializeSimpleMDE();
|
||||
}
|
||||
|
||||
@@ -443,12 +444,12 @@
|
||||
showFieldSet(customFields, false);
|
||||
showFieldSet(actionFields, true);
|
||||
actionDescription.classList.remove('hidden');
|
||||
templateDescription.classList.add('hidden');
|
||||
linkDescription.classList.add('hidden');
|
||||
customDescription.classList.add('hidden');
|
||||
linkTypeInput.value = 'ACTION';
|
||||
textField.classList.add('hidden');
|
||||
if (tplBuilder) tplBuilder.classList.add('hidden');
|
||||
if (tplToggle) tplToggle.classList.add('hidden');
|
||||
}
|
||||
|
||||
function initializeSimpleMDE() {
|
||||
@@ -508,7 +509,6 @@
|
||||
}
|
||||
|
||||
linkTab.addEventListener('click', showLinkFields);
|
||||
templateTab.addEventListener('click', showTemplateFields);
|
||||
customTab.addEventListener('click', showCustomFields);
|
||||
actionTab.addEventListener('click', showActionFields);
|
||||
|
||||
@@ -542,8 +542,6 @@
|
||||
renderPreview();
|
||||
|
||||
/* ── Sketch-style template builder ── */
|
||||
const urlInput = document.getElementById('id_original_url');
|
||||
const builder = document.getElementById('template-builder');
|
||||
const paramListEl = document.getElementById('tpl-param-list');
|
||||
const previewEl = document.getElementById('tpl-preview-url');
|
||||
const newNameInput = document.getElementById('tpl-new-name');
|
||||
@@ -739,21 +737,27 @@
|
||||
});
|
||||
}
|
||||
|
||||
// toggle: manually show/hide the builder
|
||||
if (tplToggle) {
|
||||
tplToggle.addEventListener('click', function () {
|
||||
builderManuallyOpen = !builderManuallyOpen;
|
||||
syncBuilderVisibility();
|
||||
});
|
||||
}
|
||||
|
||||
// sync on manual URL editing
|
||||
if (urlInput) urlInput.addEventListener('input', renderTemplateBuilder);
|
||||
if (urlInput) urlInput.addEventListener('input', function () {
|
||||
renderTemplateBuilder();
|
||||
syncBuilderVisibility();
|
||||
});
|
||||
renderTemplateBuilder();
|
||||
syncBuilderVisibility();
|
||||
|
||||
// Initialize form state
|
||||
if (linkTypeInput.value === 'CUSTOM') {
|
||||
showCustomFields();
|
||||
} else if (linkTypeInput.value === 'TEMPLATE') {
|
||||
showTemplateFields();
|
||||
} else if (linkTypeInput.value === 'ACTION') {
|
||||
showActionFields();
|
||||
} else if (urlInput && urlInput.value.includes('{') && urlInput.value.includes('}')) {
|
||||
// Legacy template links (LINK type + {param} in URL): auto-switch
|
||||
// to the Template tab so the visual builder is available.
|
||||
showTemplateFields();
|
||||
} else {
|
||||
showLinkFields();
|
||||
}
|
||||
|
||||
@@ -1276,7 +1276,6 @@
|
||||
}
|
||||
|
||||
function badgeClassFor(item) {
|
||||
if (item.link_type === 'TEMPLATE') return 'badge-template';
|
||||
if (item.original_url && item.original_url.includes('{') && item.original_url.includes('}')) return 'badge-template';
|
||||
if (item.link_type === 'ACTION') return 'badge-action';
|
||||
if (item.link_type === 'LINK') return 'badge-link';
|
||||
|
||||
@@ -170,7 +170,6 @@ class LinkCreateView(CreateView):
|
||||
# 新建表单:根据当前表单值判断(提交失败重渲染时保持选中状态)
|
||||
form = context.get('form')
|
||||
context['is_custom'] = bool(form and form['link_type'].value() == Link.LinkType.CUSTOM)
|
||||
context['is_template'] = bool(form and form['link_type'].value() == Link.LinkType.TEMPLATE)
|
||||
context['is_action'] = bool(form and form['link_type'].value() == Link.LinkType.ACTION)
|
||||
return context
|
||||
|
||||
@@ -235,7 +234,6 @@ class LinkUpdateView(UpdateView):
|
||||
context = super().get_context_data(**kwargs)
|
||||
link = self.get_object()
|
||||
context['is_custom'] = link.link_type == Link.LinkType.CUSTOM
|
||||
context['is_template'] = link.link_type == Link.LinkType.TEMPLATE
|
||||
context['is_action'] = link.link_type == Link.LinkType.ACTION
|
||||
return context
|
||||
|
||||
|
||||
Reference in New Issue
Block a user