mirror of
https://github.com/naturalcrit/homebrewery.git
synced 2026-03-29 07:48:11 +00:00
Merge branch 'master' into moveSnippetImages
This commit is contained in:
@@ -16,6 +16,7 @@ const Combobox = createReactClass({
|
|||||||
suggestMethod : 'includes',
|
suggestMethod : 'includes',
|
||||||
filterOn : [] // should allow as array to filter on multiple attributes, or even custom filter
|
filterOn : [] // should allow as array to filter on multiple attributes, or even custom filter
|
||||||
},
|
},
|
||||||
|
valuePatterns: /.+/
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
getInitialState : function() {
|
getInitialState : function() {
|
||||||
@@ -74,6 +75,7 @@ const Combobox = createReactClass({
|
|||||||
type='text'
|
type='text'
|
||||||
onChange={(e)=>this.handleInput(e)}
|
onChange={(e)=>this.handleInput(e)}
|
||||||
value={this.state.value || ''}
|
value={this.state.value || ''}
|
||||||
|
pattern={this.props.valuePatterns}
|
||||||
placeholder={this.props.placeholder}
|
placeholder={this.props.placeholder}
|
||||||
onBlur={(e)=>{
|
onBlur={(e)=>{
|
||||||
if(!e.target.checkValidity()){
|
if(!e.target.checkValidity()){
|
||||||
@@ -82,6 +84,12 @@ const Combobox = createReactClass({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
onKeyDown={(e)=>{
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
e.preventDefault();
|
||||||
|
this.props.onEntry(e);
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<i className='fas fa-caret-down'/>
|
<i className='fas fa-caret-down'/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -59,6 +59,12 @@
|
|||||||
}
|
}
|
||||||
&-corner { visibility : hidden; }
|
&-corner { visibility : hidden; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@supports (break-after:always) {
|
||||||
|
.columnSplit {
|
||||||
|
margin-bottom: 100vh;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.pane { position : relative; }
|
.pane { position : relative; }
|
||||||
@@ -81,4 +87,5 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.headerNav { visibility : hidden; }
|
.headerNav { visibility : hidden; }
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -10,8 +10,6 @@ import TagInput from '../tagInput/tagInput.jsx';
|
|||||||
import Themes from 'themes/themes.json';
|
import Themes from 'themes/themes.json';
|
||||||
import validations from './validations.js';
|
import validations from './validations.js';
|
||||||
|
|
||||||
const SYSTEMS = ['5e', '4e', '3.5e', 'Pathfinder'];
|
|
||||||
|
|
||||||
import homebreweryThumbnail from '../../thumbnail.png';
|
import homebreweryThumbnail from '../../thumbnail.png';
|
||||||
|
|
||||||
const callIfExists = (val, fn, ...args)=>{
|
const callIfExists = (val, fn, ...args)=>{
|
||||||
@@ -33,7 +31,6 @@ const MetadataEditor = createReactClass({
|
|||||||
tags : [],
|
tags : [],
|
||||||
published : false,
|
published : false,
|
||||||
authors : [],
|
authors : [],
|
||||||
systems : [],
|
|
||||||
renderer : 'legacy',
|
renderer : 'legacy',
|
||||||
theme : '5ePHB',
|
theme : '5ePHB',
|
||||||
lang : 'en'
|
lang : 'en'
|
||||||
@@ -91,15 +88,6 @@ const MetadataEditor = createReactClass({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
handleSystem : function(system, e){
|
|
||||||
if(e.target.checked){
|
|
||||||
this.props.metadata.systems.push(system);
|
|
||||||
} else {
|
|
||||||
this.props.metadata.systems = _.without(this.props.metadata.systems, system);
|
|
||||||
}
|
|
||||||
this.props.onChange(this.props.metadata);
|
|
||||||
},
|
|
||||||
|
|
||||||
handleRenderer : function(renderer, e){
|
handleRenderer : function(renderer, e){
|
||||||
if(e.target.checked){
|
if(e.target.checked){
|
||||||
this.props.metadata.renderer = renderer;
|
this.props.metadata.renderer = renderer;
|
||||||
@@ -155,18 +143,6 @@ const MetadataEditor = createReactClass({
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
renderSystems : function(){
|
|
||||||
return _.map(SYSTEMS, (val)=>{
|
|
||||||
return <label key={val}>
|
|
||||||
<input
|
|
||||||
type='checkbox'
|
|
||||||
checked={_.includes(this.props.metadata.systems, val)}
|
|
||||||
onChange={(e)=>this.handleSystem(val, e)} />
|
|
||||||
{val}
|
|
||||||
</label>;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
renderPublish : function(){
|
renderPublish : function(){
|
||||||
if(this.props.metadata.published){
|
if(this.props.metadata.published){
|
||||||
return <button className='unpublish' onClick={()=>this.handlePublish(false)}>
|
return <button className='unpublish' onClick={()=>this.handlePublish(false)}>
|
||||||
@@ -304,7 +280,7 @@ const MetadataEditor = createReactClass({
|
|||||||
},
|
},
|
||||||
|
|
||||||
renderRenderOptions : function(){
|
renderRenderOptions : function(){
|
||||||
return <div className='field systems'>
|
return <div className='field renderers'>
|
||||||
<label>Renderer</label>
|
<label>Renderer</label>
|
||||||
<div className='value'>
|
<div className='value'>
|
||||||
<label key='legacy'>
|
<label key='legacy'>
|
||||||
@@ -363,19 +339,15 @@ const MetadataEditor = createReactClass({
|
|||||||
{this.renderThumbnail()}
|
{this.renderThumbnail()}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<TagInput label='tags' valuePatterns={[/^(?:(?:group|meta|system|type):)?[A-Za-z0-9][A-Za-z0-9 \/.\-]{0,40}$/]}
|
<TagInput
|
||||||
|
label='tags'
|
||||||
|
valuePatterns={/^\s*(?:(?:group|meta|system|type)\s*:\s*)?[A-Za-z0-9][A-Za-z0-9 \/\\.&_\-]{0,40}\s*$/}
|
||||||
placeholder='add tag' unique={true}
|
placeholder='add tag' unique={true}
|
||||||
values={this.props.metadata.tags}
|
values={this.props.metadata.tags}
|
||||||
|
smallText='You may start tags with "type", "system", "group" or "meta" followed by a colon ":", these will be colored in your userpage.'
|
||||||
onChange={(e)=>this.handleFieldChange('tags', e)}
|
onChange={(e)=>this.handleFieldChange('tags', e)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className='field systems'>
|
|
||||||
<label>systems</label>
|
|
||||||
<div className='value'>
|
|
||||||
{this.renderSystems()}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{this.renderLanguageDropdown()}
|
{this.renderLanguageDropdown()}
|
||||||
|
|
||||||
{this.renderThemeDropdown()}
|
{this.renderThemeDropdown()}
|
||||||
@@ -386,11 +358,13 @@ const MetadataEditor = createReactClass({
|
|||||||
|
|
||||||
{this.renderAuthors()}
|
{this.renderAuthors()}
|
||||||
|
|
||||||
<TagInput label='invited authors' valuePatterns={[/.+/]}
|
<TagInput
|
||||||
|
label='invited authors'
|
||||||
|
valuePatterns={/.+/}
|
||||||
validators={[(v)=>!this.props.metadata.authors?.includes(v)]}
|
validators={[(v)=>!this.props.metadata.authors?.includes(v)]}
|
||||||
placeholder='invite author' unique={true}
|
placeholder='invite author' unique={true}
|
||||||
values={this.props.metadata.invitedAuthors}
|
values={this.props.metadata.invitedAuthors}
|
||||||
notes={['Invited author usernames are case sensitive.', 'After adding an invited author, send them the edit link. There, they can choose to accept or decline the invitation.']}
|
smallText='Invited author usernames are case sensitive. After adding an invited author, send them the edit link. There, they can choose to accept or decline the invitation.'
|
||||||
onChange={(e)=>this.handleFieldChange('invitedAuthors', e)}
|
onChange={(e)=>this.handleFieldChange('invitedAuthors', e)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -114,6 +114,11 @@
|
|||||||
z-index : 200;
|
z-index : 200;
|
||||||
max-width : 150px;
|
max-width : 150px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.tags .tagInput-dropdown {
|
||||||
|
z-index : 201;
|
||||||
|
max-width : 200px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -129,7 +134,7 @@
|
|||||||
background-color : #AAAAAA;
|
background-color : #AAAAAA;
|
||||||
}
|
}
|
||||||
|
|
||||||
.systems.field .value {
|
.renderers.field .value {
|
||||||
label {
|
label {
|
||||||
display : inline-flex;
|
display : inline-flex;
|
||||||
align-items : center;
|
align-items : center;
|
||||||
|
|||||||
210
client/homebrew/editor/tagInput/curatedTagSuggestionList.js
Normal file
210
client/homebrew/editor/tagInput/curatedTagSuggestionList.js
Normal file
@@ -0,0 +1,210 @@
|
|||||||
|
export default [
|
||||||
|
// ############################## Systems
|
||||||
|
// D&D
|
||||||
|
"system:D&D Original",
|
||||||
|
"system:D&D Basic",
|
||||||
|
"system:AD&D 1e",
|
||||||
|
"system:AD&D 2e",
|
||||||
|
"system:D&D 3e",
|
||||||
|
"system:D&D 3.5e",
|
||||||
|
"system:D&D 4e",
|
||||||
|
"system:D&D 5e",
|
||||||
|
"system:D&D 5e 2024",
|
||||||
|
"system:BD&D (B/X)",
|
||||||
|
"system:D&D Essentials",
|
||||||
|
|
||||||
|
// Other Famous RPGs
|
||||||
|
"system:Pathfinder 1e",
|
||||||
|
"system:Pathfinder 2e",
|
||||||
|
"system:Vampire: The Masquerade",
|
||||||
|
"system:Werewolf: The Apocalypse",
|
||||||
|
"system:Mage: The Ascension",
|
||||||
|
"system:Call of Cthulhu",
|
||||||
|
"system:Shadowrun",
|
||||||
|
"system:Star Wars RPG (D6/D20/Edge of the Empire)",
|
||||||
|
"system:Warhammer Fantasy Roleplay",
|
||||||
|
"system:Cyberpunk 2020",
|
||||||
|
"system:Blades in the Dark",
|
||||||
|
"system:Daggerheart",
|
||||||
|
"system:Draw Steel",
|
||||||
|
"system:Mutants and Masterminds",
|
||||||
|
|
||||||
|
// Meta
|
||||||
|
"meta:V3",
|
||||||
|
"meta:Legacy",
|
||||||
|
"meta:Template",
|
||||||
|
"meta:Theme",
|
||||||
|
"meta:free",
|
||||||
|
"meta:Character Sheet",
|
||||||
|
"meta:Documentation",
|
||||||
|
"meta:NPC",
|
||||||
|
"meta:Guide",
|
||||||
|
"meta:Resource",
|
||||||
|
"meta:Notes",
|
||||||
|
"meta:Example",
|
||||||
|
|
||||||
|
// Book type
|
||||||
|
"type:Campaign",
|
||||||
|
"type:Campaign Setting",
|
||||||
|
"type:Adventure",
|
||||||
|
"type:One-Shot",
|
||||||
|
"type:Setting",
|
||||||
|
"type:World",
|
||||||
|
"type:Lore",
|
||||||
|
"type:History",
|
||||||
|
"type:Dungeon Master",
|
||||||
|
"type:Encounter Pack",
|
||||||
|
"type:Encounter",
|
||||||
|
"type:Session Notes",
|
||||||
|
"type:reference",
|
||||||
|
"type:Handbook",
|
||||||
|
"type:Manual",
|
||||||
|
"type:Manuals",
|
||||||
|
"type:Compendium",
|
||||||
|
"type:Bestiary",
|
||||||
|
|
||||||
|
// ###################################### RPG Keywords
|
||||||
|
|
||||||
|
// Classes / Subclasses / Archetypes
|
||||||
|
"Class",
|
||||||
|
"Subclass",
|
||||||
|
"Archetype",
|
||||||
|
"Martial",
|
||||||
|
"Half-Caster",
|
||||||
|
"Full Caster",
|
||||||
|
"Artificer",
|
||||||
|
"Barbarian",
|
||||||
|
"Bard",
|
||||||
|
"Cleric",
|
||||||
|
"Druid",
|
||||||
|
"Fighter",
|
||||||
|
"Monk",
|
||||||
|
"Paladin",
|
||||||
|
"Rogue",
|
||||||
|
"Sorcerer",
|
||||||
|
"Warlock",
|
||||||
|
"Wizard",
|
||||||
|
|
||||||
|
// Races / Species / Lineages
|
||||||
|
"Race",
|
||||||
|
"Ancestry",
|
||||||
|
"Lineage",
|
||||||
|
"Aasimar",
|
||||||
|
"Beastfolk",
|
||||||
|
"Dragonborn",
|
||||||
|
"Dwarf",
|
||||||
|
"Elf",
|
||||||
|
"Goblin",
|
||||||
|
"Half-Elf",
|
||||||
|
"Half-Orc",
|
||||||
|
"Human",
|
||||||
|
"Kobold",
|
||||||
|
"Lizardfolk",
|
||||||
|
"Lycan",
|
||||||
|
"Orc",
|
||||||
|
"Tiefling",
|
||||||
|
"Vampire",
|
||||||
|
"Yuan-Ti",
|
||||||
|
|
||||||
|
// Magic / Spells / Items
|
||||||
|
"Magic",
|
||||||
|
"Magic Item",
|
||||||
|
"Magic Items",
|
||||||
|
"Wondrous Item",
|
||||||
|
"Magic Weapon",
|
||||||
|
"Artifact",
|
||||||
|
"Spell",
|
||||||
|
"Spells",
|
||||||
|
"Cantrip",
|
||||||
|
"Cantrips",
|
||||||
|
"Eldritch",
|
||||||
|
"Eldritch Invocation",
|
||||||
|
"Invocation",
|
||||||
|
"Invocations",
|
||||||
|
"Pact boon",
|
||||||
|
"Pact Boon",
|
||||||
|
"Spellcaster",
|
||||||
|
"Spellblade",
|
||||||
|
"Magical Tattoos",
|
||||||
|
"Enchantment",
|
||||||
|
"Enchanted",
|
||||||
|
"Attunement",
|
||||||
|
"Requires Attunement",
|
||||||
|
"Rune",
|
||||||
|
"Runes",
|
||||||
|
"Wand",
|
||||||
|
"Rod",
|
||||||
|
"Scroll",
|
||||||
|
"Potion",
|
||||||
|
"Potions",
|
||||||
|
"Item",
|
||||||
|
"Items",
|
||||||
|
"Bag of Holding",
|
||||||
|
|
||||||
|
// Monsters / Creatures / Enemies
|
||||||
|
"Monster",
|
||||||
|
"Creatures",
|
||||||
|
"Creature",
|
||||||
|
"Beast",
|
||||||
|
"Beasts",
|
||||||
|
"Humanoid",
|
||||||
|
"Undead",
|
||||||
|
"Fiend",
|
||||||
|
"Aberration",
|
||||||
|
"Ooze",
|
||||||
|
"Giant",
|
||||||
|
"Dragon",
|
||||||
|
"Monstrosity",
|
||||||
|
"Demon",
|
||||||
|
"Devil",
|
||||||
|
"Elemental",
|
||||||
|
"Construct",
|
||||||
|
"Constructs",
|
||||||
|
"Boss",
|
||||||
|
"BBEG",
|
||||||
|
|
||||||
|
// ############################# Media / Pop Culture
|
||||||
|
"One Piece",
|
||||||
|
"Dragon Ball",
|
||||||
|
"Dragon Ball Z",
|
||||||
|
"Naruto",
|
||||||
|
"Jujutsu Kaisen",
|
||||||
|
"Fairy Tail",
|
||||||
|
"Final Fantasy",
|
||||||
|
"Kingdom Hearts",
|
||||||
|
"Elder Scrolls",
|
||||||
|
"Skyrim",
|
||||||
|
"WoW",
|
||||||
|
"World of Warcraft",
|
||||||
|
"Marvel Comics",
|
||||||
|
"DC Comics",
|
||||||
|
"Pokemon",
|
||||||
|
"League of Legends",
|
||||||
|
"Runeterra",
|
||||||
|
"Arcane",
|
||||||
|
"Yu-Gi-Oh",
|
||||||
|
"Minecraft",
|
||||||
|
"Don't Starve",
|
||||||
|
"Witcher",
|
||||||
|
"Witcher 3",
|
||||||
|
"Cyberpunk",
|
||||||
|
"Cyberpunk 2077",
|
||||||
|
"Fallout",
|
||||||
|
"Divinity Original Sin 2",
|
||||||
|
"Fullmetal Alchemist",
|
||||||
|
"Fullmetal Alchemist Brotherhood",
|
||||||
|
"Lobotomy Corporation",
|
||||||
|
"Bloodborne",
|
||||||
|
"Dragonlance",
|
||||||
|
"Shackled City Adventure Path",
|
||||||
|
"Baldurs Gate 3",
|
||||||
|
"Library of Ruina",
|
||||||
|
"Radiant Citadel",
|
||||||
|
"Ravenloft",
|
||||||
|
"Forgotten Realms",
|
||||||
|
"Exandria",
|
||||||
|
"Critical Role",
|
||||||
|
"Star Wars",
|
||||||
|
"SW5e",
|
||||||
|
"Star Wars 5e",
|
||||||
|
];
|
||||||
@@ -1,101 +1,210 @@
|
|||||||
import './tagInput.less';
|
import './tagInput.less';
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import _ from 'lodash';
|
import Combobox from '../../../components/combobox.jsx';
|
||||||
|
|
||||||
const TagInput = ({ unique = true, values = [], ...props })=>{
|
import tagSuggestionList from './curatedTagSuggestionList.js';
|
||||||
const [tempInputText, setTempInputText] = useState('');
|
|
||||||
const [tagList, setTagList] = useState(values.map((value)=>({ value, editing: false })));
|
const TagInput = ({ label, valuePatterns, values = [], unique = true, placeholder = '', smallText = '', onChange })=>{
|
||||||
|
const [tagList, setTagList] = useState(
|
||||||
|
values.map((value)=>({
|
||||||
|
value,
|
||||||
|
editing : false,
|
||||||
|
draft : '',
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(()=>{
|
useEffect(()=>{
|
||||||
handleChange(tagList.map((context)=>context.value));
|
const incoming = values || [];
|
||||||
|
const current = tagList.map((t)=>t.value);
|
||||||
|
|
||||||
|
const changed = incoming.length !== current.length || incoming.some((v, i)=>v !== current[i]);
|
||||||
|
|
||||||
|
if(changed) {
|
||||||
|
setTagList(
|
||||||
|
incoming.map((value)=>({
|
||||||
|
value,
|
||||||
|
editing : false,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [values]);
|
||||||
|
|
||||||
|
useEffect(()=>{
|
||||||
|
onChange?.({
|
||||||
|
target : { value: tagList.map((t)=>t.value) },
|
||||||
|
});
|
||||||
}, [tagList]);
|
}, [tagList]);
|
||||||
|
|
||||||
const handleChange = (value)=>{
|
// substrings to be normalized to the first value on the array
|
||||||
props.onChange({
|
const duplicateGroups = [
|
||||||
target : { value }
|
['5e 2024', '5.5e', '5e\'24', '5.24', '5e24', '5.5'],
|
||||||
});
|
['5e', '5th Edition'],
|
||||||
};
|
['Dungeons & Dragons', 'Dungeons and Dragons', 'Dungeons n dragons'],
|
||||||
|
['D&D', 'DnD', 'dnd', 'Dnd', 'dnD', 'd&d', 'd&D', 'D&d'],
|
||||||
|
['P2e', 'p2e', 'P2E', 'Pathfinder 2e'],
|
||||||
|
];
|
||||||
|
|
||||||
const handleInputKeyDown = ({ evt, value, index, options = {} })=>{
|
const normalizeValue = (input)=>{
|
||||||
if(_.includes(['Enter', ','], evt.key)) {
|
const lowerInput = input.toLowerCase();
|
||||||
evt.preventDefault();
|
let normalizedTag = input;
|
||||||
submitTag(evt.target.value, value, index);
|
|
||||||
if(options.clear) {
|
for (const group of duplicateGroups) {
|
||||||
setTempInputText('');
|
for (const tag of group) {
|
||||||
|
if(!tag) continue;
|
||||||
|
|
||||||
|
const index = lowerInput.indexOf(tag.toLowerCase());
|
||||||
|
if(index !== -1) {
|
||||||
|
normalizedTag = input.slice(0, index) + group[0] + input.slice(index + tag.length);
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(normalizedTag.includes(':')) {
|
||||||
|
const [rawType, rawValue = ''] = normalizedTag.split(':');
|
||||||
|
const tagType = rawType.trim().toLowerCase();
|
||||||
|
const tagValue = rawValue.trim();
|
||||||
|
|
||||||
|
if(tagValue.length > 0) {
|
||||||
|
normalizedTag = `${tagType}:${tagValue[0].toUpperCase()}${tagValue.slice(1)}`;
|
||||||
|
}
|
||||||
|
//trims spaces around colon and capitalizes the first word after the colon
|
||||||
|
//this is preferred to users not understanding they can't put spaces in
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalizedTag;
|
||||||
};
|
};
|
||||||
|
|
||||||
const submitTag = (newValue, originalValue, index)=>{
|
const submitTag = (newValue, index = null)=>{
|
||||||
setTagList((prevContext)=>{
|
const trimmed = newValue?.trim();
|
||||||
// remove existing tag
|
if(!trimmed) return;
|
||||||
if(newValue === null){
|
if(!valuePatterns.test(trimmed)) return;
|
||||||
return [...prevContext].filter((context, i)=>i !== index);
|
|
||||||
|
const normalizedTag = normalizeValue(trimmed);
|
||||||
|
|
||||||
|
setTagList((prev)=>{
|
||||||
|
const existsIndex = prev.findIndex((t)=>t.value.toLowerCase() === normalizedTag.toLowerCase());
|
||||||
|
if(unique && existsIndex !== -1) return prev;
|
||||||
|
if(index !== null) {
|
||||||
|
return prev.map((t, i)=>(i === index ? { ...t, value: normalizedTag, editing: false } : t));
|
||||||
}
|
}
|
||||||
// add new tag
|
|
||||||
if(originalValue === null){
|
return [...prev, { value: normalizedTag, editing: false }];
|
||||||
return [...prevContext, { value: newValue, editing: false }];
|
|
||||||
}
|
|
||||||
// update existing tag
|
|
||||||
return prevContext.map((context, i)=>{
|
|
||||||
if(i === index) {
|
|
||||||
return { ...context, value: newValue, editing: false };
|
|
||||||
}
|
|
||||||
return context;
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const removeTag = (index)=>{
|
||||||
|
setTagList((prev)=>prev.filter((_, i)=>i !== index));
|
||||||
|
};
|
||||||
|
|
||||||
const editTag = (index)=>{
|
const editTag = (index)=>{
|
||||||
setTagList((prevContext)=>{
|
setTagList((prev)=>prev.map((t, i)=>(i === index ? { ...t, editing: true, draft: t.value } : t)));
|
||||||
return prevContext.map((context, i)=>{
|
|
||||||
if(i === index) {
|
|
||||||
return { ...context, editing: true };
|
|
||||||
}
|
|
||||||
return { ...context, editing: false };
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderReadTag = (context, index)=>{
|
const stopEditing = (index)=>{
|
||||||
return (
|
setTagList((prev)=>prev.map((t, i)=>(i === index ? { ...t, editing: false, draft: '' } : t)));
|
||||||
<li key={index}
|
|
||||||
data-value={context.value}
|
|
||||||
className='tag'
|
|
||||||
onClick={()=>editTag(index)}>
|
|
||||||
{context.value}
|
|
||||||
<button onClick={(evt)=>{evt.stopPropagation(); submitTag(null, context.value, index);}}><i className='fa fa-times fa-fw'/></button>
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderWriteTag = (context, index)=>{
|
const suggestionOptions = tagSuggestionList.map((tag)=>{
|
||||||
|
const tagType = tag.split(':');
|
||||||
|
|
||||||
|
let classes = 'item';
|
||||||
|
switch (tagType[0]) {
|
||||||
|
case 'type':
|
||||||
|
classes = 'item type';
|
||||||
|
break;
|
||||||
|
case 'group':
|
||||||
|
classes = 'item group';
|
||||||
|
break;
|
||||||
|
case 'meta':
|
||||||
|
classes = 'item meta';
|
||||||
|
break;
|
||||||
|
case 'system':
|
||||||
|
classes = 'item system';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
classes = 'item';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<input type='text'
|
<div className={classes} key={`tag-${tag}`} value={tag} data={tag} title={tag}>
|
||||||
key={index}
|
{tag}
|
||||||
defaultValue={context.value}
|
</div>
|
||||||
onKeyDown={(evt)=>handleInputKeyDown({ evt, value: context.value, index: index })}
|
|
||||||
autoFocus
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='field'>
|
<div className='field tags'>
|
||||||
<label>{props.label}</label>
|
{label && <label>{label}</label>}
|
||||||
|
|
||||||
<div className='value'>
|
<div className='value'>
|
||||||
<ul className='list'>
|
<ul className='list'>
|
||||||
{tagList.map((context, index)=>{ return context.editing ? renderWriteTag(context, index) : renderReadTag(context, index); })}
|
{tagList.map((t, i)=>t.editing ? (
|
||||||
|
<input
|
||||||
|
key={i}
|
||||||
|
type='text'
|
||||||
|
value={t.draft} // always use draft
|
||||||
|
pattern={valuePatterns.source}
|
||||||
|
onChange={(e)=>setTagList((prev)=>prev.map((tag, idx)=>(idx === i ? { ...tag, draft: e.target.value } : tag)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
onKeyDown={(e)=>{
|
||||||
|
if(e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
submitTag(t.draft, i); // submit draft
|
||||||
|
setTagList((prev)=>prev.map((tag, idx)=>(idx === i ? { ...tag, draft: '' } : tag)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if(e.key === 'Escape') {
|
||||||
|
stopEditing(i);
|
||||||
|
e.target.blur();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<li key={i} className='tag' onClick={()=>editTag(i)}>
|
||||||
|
{t.value}
|
||||||
|
<button
|
||||||
|
type='button'
|
||||||
|
onClick={(e)=>{
|
||||||
|
e.stopPropagation();
|
||||||
|
removeTag(i);
|
||||||
|
}}>
|
||||||
|
<i className='fa fa-times fa-fw' />
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
),
|
||||||
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<input
|
<Combobox
|
||||||
type='text'
|
trigger='click'
|
||||||
className='value'
|
className='tagInput-dropdown'
|
||||||
placeholder={props.placeholder}
|
default=''
|
||||||
value={tempInputText}
|
placeholder={placeholder}
|
||||||
onChange={(e)=>setTempInputText(e.target.value)}
|
options={label === 'tags' ? suggestionOptions : []}
|
||||||
onKeyDown={(evt)=>handleInputKeyDown({ evt, value: null, options: { clear: true } })}
|
autoSuggest={
|
||||||
|
label === 'tags'
|
||||||
|
? {
|
||||||
|
suggestMethod : 'startsWith',
|
||||||
|
clearAutoSuggestOnClick : true,
|
||||||
|
filterOn : ['value', 'title'],
|
||||||
|
}
|
||||||
|
: { suggestMethod: 'includes', clearAutoSuggestOnClick: true, filterOn: [] }
|
||||||
|
}
|
||||||
|
valuePatterns={valuePatterns.source}
|
||||||
|
onSelect={(value)=>submitTag(value)}
|
||||||
|
onEntry={(e)=>{
|
||||||
|
if(e.key === 'Enter') {
|
||||||
|
console.log('submit');
|
||||||
|
e.preventDefault();
|
||||||
|
submitTag(e.target.value);
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
{smallText.length !== 0 && <small>{smallText}</small>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
.list input {
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tagInput-dropdown {
|
||||||
|
.dropdown-options {
|
||||||
|
.item {
|
||||||
|
&.type {
|
||||||
|
background-color: #00800035;
|
||||||
|
}
|
||||||
|
&.group {
|
||||||
|
background-color: #50505035;
|
||||||
|
}
|
||||||
|
&.meta {
|
||||||
|
background-color: #00008035;
|
||||||
|
}
|
||||||
|
&.system {
|
||||||
|
background-color: #80000035;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
1980
client/homebrew/editor/tagInput/tagSuggestionList.js
Normal file
1980
client/homebrew/editor/tagInput/tagSuggestionList.js
Normal file
File diff suppressed because it is too large
Load Diff
997
package-lock.json
generated
997
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
21
package.json
21
package.json
@@ -61,10 +61,11 @@
|
|||||||
"server"
|
"server"
|
||||||
],
|
],
|
||||||
"transformIgnorePatterns": [
|
"transformIgnorePatterns": [
|
||||||
"node_modules/(?!(nanoid|@exodus/bytes|parse5)/)"
|
"node_modules/(?!(nanoid|@exodus/bytes|parse5|@asamuzakjp|@csstools)/)"
|
||||||
],
|
],
|
||||||
"transform": {
|
"transform": {
|
||||||
"^.+\\.js$": "babel-jest"
|
"^.+\\.[jt]s$": "babel-jest",
|
||||||
|
"^.+\\.mjs$": "babel-jest"
|
||||||
},
|
},
|
||||||
"coveragePathIgnorePatterns": [
|
"coveragePathIgnorePatterns": [
|
||||||
"build/*"
|
"build/*"
|
||||||
@@ -88,13 +89,13 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/core": "^7.28.4",
|
"@babel/core": "^7.29.0",
|
||||||
"@babel/plugin-transform-runtime": "^7.28.3",
|
"@babel/plugin-transform-runtime": "^7.29.0",
|
||||||
"@babel/preset-env": "^7.28.3",
|
"@babel/preset-env": "^7.29.0",
|
||||||
"@babel/preset-react": "^7.28.5",
|
"@babel/preset-react": "^7.28.5",
|
||||||
"@babel/runtime": "^7.28.4",
|
"@babel/runtime": "^7.28.4",
|
||||||
"@dmsnell/diff-match-patch": "^1.1.0",
|
"@dmsnell/diff-match-patch": "^1.1.0",
|
||||||
"@googleapis/drive": "^19.2.0",
|
"@googleapis/drive": "^20.1.0",
|
||||||
"@sanity/diff-match-patch": "^3.2.0",
|
"@sanity/diff-match-patch": "^3.2.0",
|
||||||
"body-parser": "^2.2.0",
|
"body-parser": "^2.2.0",
|
||||||
"classnames": "^2.5.1",
|
"classnames": "^2.5.1",
|
||||||
@@ -109,7 +110,7 @@
|
|||||||
"express-async-handler": "^1.2.0",
|
"express-async-handler": "^1.2.0",
|
||||||
"express-static-gzip": "3.0.0",
|
"express-static-gzip": "3.0.0",
|
||||||
"fflate": "^0.8.2",
|
"fflate": "^0.8.2",
|
||||||
"fs-extra": "11.3.2",
|
"fs-extra": "^11.3.3",
|
||||||
"hash-wasm": "^4.12.0",
|
"hash-wasm": "^4.12.0",
|
||||||
"idb-keyval": "^6.2.2",
|
"idb-keyval": "^6.2.2",
|
||||||
"js-yaml": "^4.1.1",
|
"js-yaml": "^4.1.1",
|
||||||
@@ -125,10 +126,10 @@
|
|||||||
"marked-nonbreaking-spaces": "^1.0.1",
|
"marked-nonbreaking-spaces": "^1.0.1",
|
||||||
"marked-smartypants-lite": "^1.0.3",
|
"marked-smartypants-lite": "^1.0.3",
|
||||||
"marked-subsuper-text": "^1.0.4",
|
"marked-subsuper-text": "^1.0.4",
|
||||||
"marked-variables": "^1.0.4",
|
"marked-variables": "^1.0.5",
|
||||||
"markedLegacy": "npm:marked@^0.3.19",
|
"markedLegacy": "npm:marked@^0.3.19",
|
||||||
"moment": "^2.30.1",
|
"moment": "^2.30.1",
|
||||||
"mongoose": "^8.20.0",
|
"mongoose": "^9.2.1",
|
||||||
"nanoid": "5.1.6",
|
"nanoid": "5.1.6",
|
||||||
"nconf": "^0.13.0",
|
"nconf": "^0.13.0",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
@@ -151,7 +152,7 @@
|
|||||||
"globals": "^16.4.0",
|
"globals": "^16.4.0",
|
||||||
"jest": "^30.2.0",
|
"jest": "^30.2.0",
|
||||||
"jest-expect-message": "^1.1.3",
|
"jest-expect-message": "^1.1.3",
|
||||||
"jsdom": "^27.4.0",
|
"jsdom": "^28.1.0",
|
||||||
"jsdom-global": "^3.0.2",
|
"jsdom-global": "^3.0.2",
|
||||||
"postcss-less": "^6.0.0",
|
"postcss-less": "^6.0.0",
|
||||||
"stylelint": "^16.25.0",
|
"stylelint": "^16.25.0",
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
/*eslint max-lines: ["warn", {"max": 1000, "skipBlankLines": true, "skipComments": true}]*/
|
/*eslint max-lines: ["warn", {"max": 1000, "skipBlankLines": true, "skipComments": true}]*/
|
||||||
|
import mongoose from 'mongoose';
|
||||||
import supertest from 'supertest';
|
import supertest from 'supertest';
|
||||||
import HBApp from './app.js';
|
import HBApp from './app.js';
|
||||||
import { model as NotificationModel } from './notifications.model.js';
|
import { model as NotificationModel } from './notifications.model.js';
|
||||||
@@ -8,8 +9,19 @@ import { model as HomebrewModel } from './homebrew.model.js';
|
|||||||
// Mimic https responses to avoid being redirected all the time
|
// Mimic https responses to avoid being redirected all the time
|
||||||
const app = supertest.agent(HBApp).set('X-Forwarded-Proto', 'https');
|
const app = supertest.agent(HBApp).set('X-Forwarded-Proto', 'https');
|
||||||
|
|
||||||
|
let dbState;
|
||||||
|
|
||||||
describe('Tests for admin api', ()=>{
|
describe('Tests for admin api', ()=>{
|
||||||
|
beforeEach(()=>{
|
||||||
|
// Mock DB ready (for dbCheck middleware)
|
||||||
|
dbState = mongoose.connection.readyState;
|
||||||
|
mongoose.connection.readyState = 1;
|
||||||
|
});
|
||||||
|
|
||||||
afterEach(()=>{
|
afterEach(()=>{
|
||||||
|
// Restore DB ready state
|
||||||
|
mongoose.connection.readyState = dbState;
|
||||||
|
|
||||||
jest.resetAllMocks();
|
jest.resetAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -588,6 +588,21 @@ export default [
|
|||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name : 'True 20',
|
||||||
|
subsnippets : [
|
||||||
|
{
|
||||||
|
name : 'OGL 1.0 Section 15',
|
||||||
|
gen : LicenseGen.grTrue20Sec15,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name : 'True20 Logo',
|
||||||
|
gen : LicenseGen.grTrue20CompatLogo,
|
||||||
|
}
|
||||||
|
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
name : 'Wizards of the Coast',
|
name : 'Wizards of the Coast',
|
||||||
icon : 'fab fa-wizards-of-the-coast',
|
icon : 'fab fa-wizards-of-the-coast',
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user