add old lexicon merge
This commit is contained in:
@@ -175,6 +175,24 @@ export function renderRecordList(
|
|||||||
`
|
`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if a record needs migration to new format
|
||||||
|
function needsMigration(value: unknown, collection: string): boolean {
|
||||||
|
if (!value || typeof value !== 'object') return false
|
||||||
|
const v = value as Record<string, unknown>
|
||||||
|
|
||||||
|
// Only for ai.syui.log.post and ai.syui.log.chat
|
||||||
|
if (!collection.startsWith('ai.syui.log.')) return false
|
||||||
|
|
||||||
|
// Check: content is a string (old) instead of object (new)
|
||||||
|
if (typeof v.content === 'string') return true
|
||||||
|
// Check: has createdAt but no publishedAt
|
||||||
|
if (v.createdAt && !v.publishedAt) return true
|
||||||
|
// Check: missing site field
|
||||||
|
if (!v.site) return true
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// Render single record detail
|
// Render single record detail
|
||||||
export function renderRecordDetail(
|
export function renderRecordDetail(
|
||||||
record: { uri: string; cid: string; value: unknown },
|
record: { uri: string; cid: string; value: unknown },
|
||||||
@@ -186,12 +204,18 @@ export function renderRecordDetail(
|
|||||||
<button type="button" class="record-delete-btn" id="record-delete-btn" data-collection="${collection}" data-rkey="${rkey}">Delete</button>
|
<button type="button" class="record-delete-btn" id="record-delete-btn" data-collection="${collection}" data-rkey="${rkey}">Delete</button>
|
||||||
` : ''
|
` : ''
|
||||||
|
|
||||||
|
const showMerge = isOwner && needsMigration(record.value, collection)
|
||||||
|
const mergeBtn = showMerge ? `
|
||||||
|
<button type="button" class="record-merge-btn" id="record-merge-btn" data-collection="${collection}" data-rkey="${rkey}">Merge</button>
|
||||||
|
` : ''
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<article class="record-detail">
|
<article class="record-detail">
|
||||||
<header class="record-header">
|
<header class="record-header">
|
||||||
<div class="record-header-top">
|
<div class="record-header-top">
|
||||||
<h3>${collection}</h3>
|
<h3>${collection}</h3>
|
||||||
<button type="button" class="validate-btn" id="validate-btn" data-collection="${collection}">Validate</button>
|
<button type="button" class="validate-btn" id="validate-btn" data-collection="${collection}">Validate</button>
|
||||||
|
${mergeBtn}
|
||||||
</div>
|
</div>
|
||||||
<p class="record-uri">URI: ${record.uri}</p>
|
<p class="record-uri">URI: ${record.uri}</p>
|
||||||
<p class="record-cid">CID: ${record.cid}</p>
|
<p class="record-cid">CID: ${record.cid}</p>
|
||||||
|
|||||||
@@ -3,6 +3,17 @@ import { renderMarkdown } from '../lib/markdown'
|
|||||||
import { renderDiscussion, loadDiscussionPosts } from './discussion'
|
import { renderDiscussion, loadDiscussionPosts } from './discussion'
|
||||||
import { getCurrentLang } from './mode-tabs'
|
import { getCurrentLang } from './mode-tabs'
|
||||||
|
|
||||||
|
// Extract text content from post content (handles both string and object formats)
|
||||||
|
function getContentText(content: unknown): string {
|
||||||
|
if (!content) return ''
|
||||||
|
if (typeof content === 'string') return content
|
||||||
|
if (typeof content === 'object' && content !== null) {
|
||||||
|
const obj = content as Record<string, unknown>
|
||||||
|
if (typeof obj.text === 'string') return obj.text
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
// Format date as yyyy/mm/dd
|
// Format date as yyyy/mm/dd
|
||||||
function formatDate(dateStr: string): string {
|
function formatDate(dateStr: string): string {
|
||||||
const d = new Date(dateStr)
|
const d = new Date(dateStr)
|
||||||
@@ -61,6 +72,8 @@ export function renderPostDetail(
|
|||||||
// Build post URL for discussion search
|
// Build post URL for discussion search
|
||||||
const postUrl = siteUrl ? `${siteUrl}/@${handle}/${rkey}` : `${window.location.origin}/@${handle}/${rkey}`
|
const postUrl = siteUrl ? `${siteUrl}/@${handle}/${rkey}` : `${window.location.origin}/@${handle}/${rkey}`
|
||||||
|
|
||||||
|
const rawContent = getContentText(post.value.content)
|
||||||
|
|
||||||
const editBtn = isOwner ? `<button type="button" class="post-edit-btn" id="post-edit-btn">Edit</button>` : ''
|
const editBtn = isOwner ? `<button type="button" class="post-edit-btn" id="post-edit-btn">Edit</button>` : ''
|
||||||
|
|
||||||
// Get current language and show appropriate content
|
// Get current language and show appropriate content
|
||||||
@@ -68,22 +81,22 @@ export function renderPostDetail(
|
|||||||
const translations = post.value.translations
|
const translations = post.value.translations
|
||||||
const originalLang = post.value.langs?.[0] || 'ja'
|
const originalLang = post.value.langs?.[0] || 'ja'
|
||||||
|
|
||||||
let displayTitle = post.value.title
|
let displayTitle = post.value.title || ''
|
||||||
let displayContent = post.value.content.text
|
let displayContent = rawContent
|
||||||
|
|
||||||
// Use translation if available and not original language
|
// Use translation if available and not original language
|
||||||
if (translations && currentLang !== originalLang && translations[currentLang]) {
|
if (translations && currentLang !== originalLang && translations[currentLang]) {
|
||||||
const trans = translations[currentLang]
|
const trans = translations[currentLang]
|
||||||
displayTitle = trans.title || post.value.title
|
displayTitle = trans.title || post.value.title || ''
|
||||||
displayContent = trans.content
|
displayContent = trans.content || rawContent
|
||||||
}
|
}
|
||||||
|
|
||||||
const content = renderMarkdown(displayContent)
|
const content = displayContent ? renderMarkdown(displayContent) : ''
|
||||||
|
|
||||||
const editForm = isOwner ? `
|
const editForm = isOwner ? `
|
||||||
<div class="post-edit-form" id="post-edit-form" style="display: none;">
|
<div class="post-edit-form" id="post-edit-form" style="display: none;">
|
||||||
<input type="text" class="post-edit-title" id="post-edit-title" value="${escapeHtml(post.value.title)}" placeholder="Title">
|
<input type="text" class="post-edit-title" id="post-edit-title" value="${escapeHtml(post.value.title || '')}" placeholder="Title">
|
||||||
<textarea class="post-edit-content" id="post-edit-content" rows="15">${escapeHtml(post.value.content.text)}</textarea>
|
<textarea class="post-edit-content" id="post-edit-content" rows="15">${escapeHtml(rawContent)}</textarea>
|
||||||
<div class="post-edit-actions">
|
<div class="post-edit-actions">
|
||||||
<button type="button" class="post-edit-cancel" id="post-edit-cancel">Cancel</button>
|
<button type="button" class="post-edit-cancel" id="post-edit-cancel">Cancel</button>
|
||||||
<button type="button" class="post-edit-save" id="post-edit-save" data-collection="${collection}" data-rkey="${rkey}">Save</button>
|
<button type="button" class="post-edit-save" id="post-edit-save" data-collection="${collection}" data-rkey="${rkey}">Save</button>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import './styles/card.css'
|
|||||||
import './styles/card-migrate.css'
|
import './styles/card-migrate.css'
|
||||||
import { getConfig, resolveHandle, getProfile, getPosts, getPost, describeRepo, listRecords, getRecord, getPds, getNetworks, getChatMessages, getCards, getCardAdmin, getRse, getRseAdmin, getLinks } from './lib/api'
|
import { getConfig, resolveHandle, getProfile, getPosts, getPost, describeRepo, listRecords, getRecord, getPds, getNetworks, getChatMessages, getCards, getCardAdmin, getRse, getRseAdmin, getLinks } from './lib/api'
|
||||||
import { parseRoute, onRouteChange, navigate, type Route } from './lib/router'
|
import { parseRoute, onRouteChange, navigate, type Route } from './lib/router'
|
||||||
import { login, logout, handleCallback, restoreSession, isLoggedIn, getLoggedInHandle, getLoggedInDid, deleteRecord, updatePost, updateChat, updateLinks } from './lib/auth'
|
import { login, logout, handleCallback, restoreSession, isLoggedIn, getLoggedInHandle, getLoggedInDid, deleteRecord, updatePost, updateChat, updateLinks, getAgent } from './lib/auth'
|
||||||
import { validateRecord } from './lib/lexicon'
|
import { validateRecord } from './lib/lexicon'
|
||||||
import { renderHeader } from './components/header'
|
import { renderHeader } from './components/header'
|
||||||
import { renderProfile } from './components/profile'
|
import { renderProfile } from './components/profile'
|
||||||
@@ -446,6 +446,11 @@ async function render(route: Route): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Setup merge button for AT-Browser record detail
|
||||||
|
if (route.type === 'record' && isOwner) {
|
||||||
|
setupRecordMerge()
|
||||||
|
}
|
||||||
|
|
||||||
// Setup card migration button
|
// Setup card migration button
|
||||||
if (route.type === 'card-old' && cardMigrationState?.oldApiUser && cardMigrationState?.oldApiCards) {
|
if (route.type === 'card-old' && cardMigrationState?.oldApiUser && cardMigrationState?.oldApiCards) {
|
||||||
setupMigrationButton(
|
setupMigrationButton(
|
||||||
@@ -629,6 +634,94 @@ function setupPostEdit(collection: string): void {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Setup merge button for AT-Browser record detail (convert old format to new)
|
||||||
|
function setupRecordMerge(): void {
|
||||||
|
const mergeBtn = document.getElementById('record-merge-btn')
|
||||||
|
if (!mergeBtn) return
|
||||||
|
|
||||||
|
mergeBtn.addEventListener('click', async () => {
|
||||||
|
const collection = mergeBtn.getAttribute('data-collection') || ''
|
||||||
|
const rkey = mergeBtn.getAttribute('data-rkey') || ''
|
||||||
|
if (!collection || !rkey) return
|
||||||
|
|
||||||
|
const agent = getAgent()
|
||||||
|
if (!agent) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Fetch current record
|
||||||
|
const existing = await agent.com.atproto.repo.getRecord({
|
||||||
|
repo: agent.assertDid,
|
||||||
|
collection,
|
||||||
|
rkey,
|
||||||
|
})
|
||||||
|
|
||||||
|
const oldValue = existing.data.value as Record<string, unknown>
|
||||||
|
|
||||||
|
// Build new format record
|
||||||
|
const newRecord: Record<string, unknown> = {
|
||||||
|
$type: collection,
|
||||||
|
site: (typeof oldValue.site === 'string' && oldValue.site) || window.location.origin,
|
||||||
|
title: (typeof oldValue.title === 'string' ? oldValue.title : '') || '',
|
||||||
|
publishedAt: (typeof oldValue.publishedAt === 'string' && oldValue.publishedAt)
|
||||||
|
|| (typeof oldValue.createdAt === 'string' && oldValue.createdAt)
|
||||||
|
|| new Date().toISOString(),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert content to open union format
|
||||||
|
const oldContent = oldValue.content
|
||||||
|
if (oldContent && typeof oldContent === 'object' && (oldContent as Record<string, unknown>).$type) {
|
||||||
|
// Already new format
|
||||||
|
newRecord.content = oldContent
|
||||||
|
} else if (typeof oldContent === 'string') {
|
||||||
|
// Old string format -> convert to union
|
||||||
|
newRecord.content = {
|
||||||
|
$type: `${collection}#markdown`,
|
||||||
|
text: oldContent,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
newRecord.content = {
|
||||||
|
$type: `${collection}#markdown`,
|
||||||
|
text: '',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Preserve optional fields
|
||||||
|
if (oldValue.description) newRecord.description = oldValue.description
|
||||||
|
if (oldValue.tags) newRecord.tags = oldValue.tags
|
||||||
|
if (oldValue.langs) newRecord.langs = oldValue.langs
|
||||||
|
if (oldValue.translations) newRecord.translations = oldValue.translations
|
||||||
|
if (oldValue.path) newRecord.path = oldValue.path
|
||||||
|
if (oldValue.root) newRecord.root = oldValue.root
|
||||||
|
if (oldValue.parent) newRecord.parent = oldValue.parent
|
||||||
|
if (oldValue.updatedAt) newRecord.updatedAt = oldValue.updatedAt
|
||||||
|
|
||||||
|
if (!confirm(`Merge record to new format?\n\nCollection: ${collection}\nRkey: ${rkey}`)) return
|
||||||
|
|
||||||
|
mergeBtn.textContent = 'Merging...'
|
||||||
|
;(mergeBtn as HTMLButtonElement).disabled = true
|
||||||
|
|
||||||
|
await agent.com.atproto.repo.putRecord({
|
||||||
|
repo: agent.assertDid,
|
||||||
|
collection,
|
||||||
|
rkey,
|
||||||
|
record: newRecord,
|
||||||
|
})
|
||||||
|
|
||||||
|
mergeBtn.textContent = 'Merged!'
|
||||||
|
|
||||||
|
// Refresh page after short delay
|
||||||
|
setTimeout(() => {
|
||||||
|
render(parseRoute())
|
||||||
|
}, 1000)
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Merge failed:', err)
|
||||||
|
alert('Merge failed: ' + err)
|
||||||
|
mergeBtn.textContent = 'Merge'
|
||||||
|
;(mergeBtn as HTMLButtonElement).disabled = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// Setup chat edit form
|
// Setup chat edit form
|
||||||
function setupChatEdit(collection: string, handle: string): void {
|
function setupChatEdit(collection: string, handle: string): void {
|
||||||
const form = document.getElementById('chat-edit-form') as HTMLFormElement
|
const form = document.getElementById('chat-edit-form') as HTMLFormElement
|
||||||
|
|||||||
@@ -684,6 +684,30 @@ body {
|
|||||||
opacity: 0.85;
|
opacity: 0.85;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.record-merge-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 4px 10px;
|
||||||
|
margin-left: 8px;
|
||||||
|
background: var(--btn-color);
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.record-merge-btn:hover {
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
|
|
||||||
|
.record-merge-btn:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
/* Post Edit Form */
|
/* Post Edit Form */
|
||||||
.post-edit-form {
|
.post-edit-form {
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
|
|||||||
Reference in New Issue
Block a user