🎨 Refactor components to use modular structure and computed properties for improved maintainability and reusability in Gamemaster Explorer and FilterPanel
This commit is contained in:
@@ -64,9 +64,16 @@ const props = defineProps({
|
|||||||
data: {
|
data: {
|
||||||
type: Array,
|
type: Array,
|
||||||
default: () => []
|
default: () => []
|
||||||
|
},
|
||||||
|
filterState: {
|
||||||
|
type: Object,
|
||||||
|
default: null
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const internalFilterState = useJsonFilter();
|
||||||
|
const activeFilterState = computed(() => props.filterState || internalFilterState);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
filterProperty,
|
filterProperty,
|
||||||
filterValue,
|
filterValue,
|
||||||
@@ -79,7 +86,7 @@ const {
|
|||||||
setFilter,
|
setFilter,
|
||||||
clearFilters,
|
clearFilters,
|
||||||
getUniqueValues
|
getUniqueValues
|
||||||
} = useJsonFilter();
|
} = activeFilterState.value;
|
||||||
|
|
||||||
const hasData = computed(
|
const hasData = computed(
|
||||||
() => Array.isArray(props.data) && props.data.length > 0
|
() => Array.isArray(props.data) && props.data.length > 0
|
||||||
|
|||||||
@@ -90,120 +90,17 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- File Selector -->
|
<!-- File Selector -->
|
||||||
<div class="file-selector">
|
<FileSelector :files-state="filesState" />
|
||||||
<label for="file-select">Select File:</label>
|
|
||||||
<select id="file-select" v-model="selectedFile" @change="onFileChange">
|
|
||||||
<option value="">-- Choose a file --</option>
|
|
||||||
<option
|
|
||||||
v-for="file in uniqueFiles"
|
|
||||||
:key="file.filename"
|
|
||||||
:value="getFileType(file.filename)"
|
|
||||||
>
|
|
||||||
{{ formatFileName(file.filename) }} ({{ formatSize(file.size) }})
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
<span v-if="fileContent" class="file-info"
|
|
||||||
>{{ fileLines.length.toLocaleString() }} lines</span
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Search Bar -->
|
<SearchBar
|
||||||
<div v-if="fileContent" class="search-bar">
|
v-if="fileContent"
|
||||||
<div class="search-input-wrapper">
|
:file-lines="fileLines"
|
||||||
<input
|
:display-lines="displayLines"
|
||||||
ref="searchInput"
|
:search-state="searchState"
|
||||||
type="text"
|
|
||||||
v-model="searchQuery"
|
|
||||||
@input="onSearchInput"
|
|
||||||
placeholder="Search in file... (Ctrl+F)"
|
|
||||||
class="search-input"
|
|
||||||
/>
|
/>
|
||||||
<button
|
|
||||||
v-if="searchQuery"
|
|
||||||
@click="clearSearch"
|
|
||||||
class="btn-clear"
|
|
||||||
title="Clear search"
|
|
||||||
>
|
|
||||||
✕
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="searchResults.length > 0" class="search-results">
|
|
||||||
<span :title="`Line ${searchResults[currentResultIndex] + 1}`">
|
|
||||||
{{ currentResultIndex + 1 }} / {{ searchResults.length }} (Line
|
|
||||||
{{ searchResults[currentResultIndex] + 1 }})
|
|
||||||
</span>
|
|
||||||
<button
|
|
||||||
@click="goToPrevResult"
|
|
||||||
class="btn-nav"
|
|
||||||
:disabled="searchResults.length === 0"
|
|
||||||
title="Previous result (Shift+Ctrl+G)"
|
|
||||||
>
|
|
||||||
↑
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
@click="goToNextResult"
|
|
||||||
class="btn-nav"
|
|
||||||
:disabled="searchResults.length === 0"
|
|
||||||
title="Next result (Ctrl+G)"
|
|
||||||
>
|
|
||||||
↓
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
v-if="searchHistory.history.value.length > 0"
|
|
||||||
class="search-history"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
v-for="(item, index) in searchHistory.history.value"
|
|
||||||
:key="index"
|
|
||||||
@click="applyHistoryItem(item)"
|
|
||||||
class="history-item"
|
|
||||||
>
|
|
||||||
{{ item }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Property Filter -->
|
<!-- Property Filter -->
|
||||||
<div v-if="fileContent && jsonPaths.length > 0" class="property-filter">
|
<FilterPanel v-if="fileContent" :data="filterData" :filter-state="filterState" />
|
||||||
<label for="property-select">Filter by Property:</label>
|
|
||||||
<select
|
|
||||||
id="property-select"
|
|
||||||
v-model="filterProperty"
|
|
||||||
@change="onFilterChange"
|
|
||||||
>
|
|
||||||
<option value="">All Properties</option>
|
|
||||||
<option v-for="path in jsonPaths" :key="path" :value="path">
|
|
||||||
{{ path }}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
<input
|
|
||||||
v-if="filterProperty"
|
|
||||||
type="text"
|
|
||||||
v-model="filterValue"
|
|
||||||
@input="onFilterChange"
|
|
||||||
placeholder="Enter value..."
|
|
||||||
class="filter-value-input"
|
|
||||||
/>
|
|
||||||
<label v-if="filterProperty" class="filter-mode">
|
|
||||||
<input
|
|
||||||
type="radio"
|
|
||||||
value="OR"
|
|
||||||
v-model="filterMode"
|
|
||||||
@change="onFilterChange"
|
|
||||||
/>
|
|
||||||
OR
|
|
||||||
<input
|
|
||||||
type="radio"
|
|
||||||
value="AND"
|
|
||||||
v-model="filterMode"
|
|
||||||
@change="onFilterChange"
|
|
||||||
/>
|
|
||||||
AND
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Progress Bar (for long operations) -->
|
<!-- Progress Bar (for long operations) -->
|
||||||
<div v-if="operationProgress.active" class="progress-bar-container">
|
<div v-if="operationProgress.active" class="progress-bar-container">
|
||||||
@@ -220,100 +117,28 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- JSON Content Viewer -->
|
<!-- JSON Content Viewer -->
|
||||||
<div
|
<JsonViewer
|
||||||
v-if="fileContent"
|
v-if="fileContent"
|
||||||
class="content-viewer"
|
:display-lines="displayLines"
|
||||||
:class="{
|
:file-content="fileContent"
|
||||||
'dark-mode': preferences.darkMode,
|
:selected-file="selectedFile"
|
||||||
'line-wrap': preferences.lineWrap
|
:file-too-large="fileTooLarge"
|
||||||
}"
|
:preferences="preferences"
|
||||||
>
|
:search-results="searchResults"
|
||||||
<!-- Virtual Scroller for large files -->
|
:current-result-index="currentResultIndex"
|
||||||
<RecycleScroller
|
:highlight-config="highlightConfig"
|
||||||
v-if="displayLines.length > 1000"
|
:line-height="lineHeight"
|
||||||
ref="virtualScroller"
|
:selection-state="selectionState"
|
||||||
class="scroller"
|
/>
|
||||||
:items="displayLines"
|
|
||||||
:item-size="lineHeight"
|
|
||||||
key-field="lineNumber"
|
|
||||||
>
|
|
||||||
<template #default="{ item }">
|
|
||||||
<div
|
|
||||||
:data-line="item.lineNumber"
|
|
||||||
:class="[
|
|
||||||
'line',
|
|
||||||
{ selected: selectedLines.has(item.lineNumber) },
|
|
||||||
{ 'highlight-match': item.hasMatch },
|
|
||||||
{
|
|
||||||
'current-result':
|
|
||||||
item.lineNumber === searchResults[currentResultIndex] + 1
|
|
||||||
}
|
|
||||||
]"
|
|
||||||
@click="toggleLineSelection(item.lineNumber, $event)"
|
|
||||||
>
|
|
||||||
<span v-if="preferences.showLineNumbers" class="line-number">{{
|
|
||||||
item.lineNumber
|
|
||||||
}}</span>
|
|
||||||
<pre
|
|
||||||
v-highlight="highlightConfig"
|
|
||||||
><code>{{ item.content }}</code></pre>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</RecycleScroller>
|
|
||||||
|
|
||||||
<!-- Regular render for smaller files -->
|
|
||||||
<div v-else class="lines-container">
|
|
||||||
<div
|
|
||||||
v-for="line in displayLines"
|
|
||||||
:key="line.lineNumber"
|
|
||||||
:data-line="line.lineNumber"
|
|
||||||
:class="[
|
|
||||||
'line',
|
|
||||||
{ selected: selectedLines.has(line.lineNumber) },
|
|
||||||
{ 'highlight-match': line.hasMatch },
|
|
||||||
{
|
|
||||||
'current-result':
|
|
||||||
line.lineNumber === searchResults[currentResultIndex] + 1
|
|
||||||
}
|
|
||||||
]"
|
|
||||||
@click="toggleLineSelection(line.lineNumber, $event)"
|
|
||||||
>
|
|
||||||
<span v-if="preferences.showLineNumbers" class="line-number">{{
|
|
||||||
line.lineNumber
|
|
||||||
}}</span>
|
|
||||||
<pre
|
|
||||||
v-highlight="highlightConfig"
|
|
||||||
><code>{{ line.content }}</code></pre>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Too Large Warning -->
|
|
||||||
<div v-if="fileTooLarge" class="warning-banner">
|
|
||||||
⚠️ File exceeds 10,000 lines. Showing first 10,000 lines only. Use
|
|
||||||
search or filters to find specific content.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Action Bar -->
|
<!-- Action Bar -->
|
||||||
<div v-if="fileContent" class="action-bar">
|
<ActionToolbar
|
||||||
<button
|
v-if="fileContent"
|
||||||
@click="copySelected"
|
:display-lines="displayLines"
|
||||||
:disabled="selectedLines.size === 0"
|
:file-content="fileContent"
|
||||||
class="btn-action"
|
:selected-file="selectedFile"
|
||||||
>
|
:selection-state="selectionState"
|
||||||
📋 Copy Selected ({{ selectedLines.size }} lines)
|
/>
|
||||||
</button>
|
|
||||||
<button @click="copyAll" class="btn-action">📋 Copy All</button>
|
|
||||||
<button
|
|
||||||
@click="exportSelected"
|
|
||||||
:disabled="selectedLines.size === 0"
|
|
||||||
class="btn-action"
|
|
||||||
>
|
|
||||||
💾 Export Selected
|
|
||||||
</button>
|
|
||||||
<button @click="exportAll" class="btn-action">💾 Export All</button>
|
|
||||||
<button @click="shareUrl" class="btn-action">🔗 Share Link</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Toast Notifications -->
|
<!-- Toast Notifications -->
|
||||||
<div v-if="clipboard.copied.value" class="toast success">
|
<div v-if="clipboard.copied.value" class="toast success">
|
||||||
|
|||||||
Reference in New Issue
Block a user