Compare commits
5 Commits
14ca22f3e9
...
9920a0a596
Author | SHA1 | Date | |
---|---|---|---|
9920a0a596 | |||
ec32489de5 | |||
78a197d5e4 | |||
fad3bc835b | |||
9d3ed248b3 |
36
api/api/snippets.py
Normal file
36
api/api/snippets.py
Normal file
@ -0,0 +1,36 @@
|
||||
import datetime
|
||||
from bson.objectid import ObjectId
|
||||
from util import database, util
|
||||
|
||||
def list_for_user(user):
|
||||
db = database.get_db()
|
||||
snippets = db.snippets.find({'user': user['_id']}).sort('createdAt', -1)
|
||||
return {'snippets': list(snippets)}
|
||||
|
||||
def create(user, data):
|
||||
if not data: raise util.errors.BadRequest('Invalid request')
|
||||
name = data.get('name', '')
|
||||
snippet_type = data.get('type', '')
|
||||
if len(name) < 3: raise util.errors.BadRequest('A longer name is required')
|
||||
if snippet_type not in ['warp', 'weft']:
|
||||
raise util.errors.BadRequest('Invalid snippet type')
|
||||
db = database.get_db()
|
||||
snippet = {
|
||||
'name': name,
|
||||
'user': user['_id'],
|
||||
'createdAt': datetime.datetime.utcnow(),
|
||||
'type': snippet_type,
|
||||
'threading': data.get('threading', []),
|
||||
'treadling': data.get('treadling', []),
|
||||
}
|
||||
result = db.snippets.insert_one(snippet)
|
||||
snippet['_id'] = result.inserted_id
|
||||
return snippet
|
||||
|
||||
def delete(user, id):
|
||||
db = database.get_db()
|
||||
snippet = db.snippets.find_one({'_id': ObjectId(id), 'user': user['_id']})
|
||||
if not snippet:
|
||||
raise util.errors.NotFound('Snippet not found')
|
||||
db.snippets.remove({'_id': snippet['_id']})
|
||||
return {'deletedSnippet': snippet['_id'] }
|
15
api/app.py
15
api/app.py
@ -6,7 +6,7 @@ import werkzeug
|
||||
import sentry_sdk
|
||||
from sentry_sdk.integrations.flask import FlaskIntegration
|
||||
from util import util
|
||||
from api import accounts, users, projects, objects, uploads, groups, search, invitations, root, activitypub
|
||||
from api import accounts, users, projects, objects, snippets, uploads, groups, search, invitations, root, activitypub
|
||||
|
||||
app = Flask(__name__)
|
||||
CORS(app)
|
||||
@ -188,6 +188,19 @@ def object_comments(id):
|
||||
def object_comment(id, comment_id):
|
||||
return util.jsonify(objects.delete_comment(util.get_user(), id, comment_id))
|
||||
|
||||
# SNIPPETS
|
||||
@app.route('/snippets', methods=['POST', 'GET'])
|
||||
def snippets_route():
|
||||
if request.method == 'POST':
|
||||
return util.jsonify(snippets.create(util.get_user(), request.json))
|
||||
if request.method == 'GET':
|
||||
return util.jsonify(snippets.list_for_user(util.get_user()))
|
||||
|
||||
@app.route('/snippets/<id>', methods=['DELETE'])
|
||||
def snippet_route(id):
|
||||
if request.method == 'DELETE':
|
||||
return util.jsonify(snippets.delete(util.get_user(), id))
|
||||
|
||||
# GROUPS
|
||||
|
||||
@app.route('/groups', methods=['POST', 'GET'])
|
||||
|
@ -8,6 +8,8 @@ export default {
|
||||
DELETE_OBJECT: 'DELETE_OBJECT',
|
||||
SELECT_OBJECT: 'SELECT_OBJECT',
|
||||
UPDATE_EDITOR: 'UPDATE_EDITOR',
|
||||
RECEIVE_SNIPPET: 'RECEIVE_SNIPPET',
|
||||
DELETE_SNIPPET: 'DELETE_SNIPPET',
|
||||
RECEIVE_COMMENT: 'RECEIVE_COMMENT',
|
||||
DELETE_COMMENT: 'DELETE_COMMENT',
|
||||
|
||||
@ -45,6 +47,14 @@ export default {
|
||||
return { type: this.UPDATE_EDITOR, editor };
|
||||
},
|
||||
|
||||
receiveSnippet(snippet) {
|
||||
return { type: this.RECEIVE_SNIPPET, snippet };
|
||||
},
|
||||
|
||||
deleteSnippet(snippetId) {
|
||||
return { type: this.DELETE_SNIPPET, snippetId };
|
||||
},
|
||||
|
||||
receiveComment(comment) {
|
||||
return { type: this.RECEIVE_COMMENT, comment };
|
||||
},
|
||||
|
@ -5,6 +5,7 @@ import { accounts } from './accounts';
|
||||
import { users } from './users';
|
||||
import { projects } from './projects';
|
||||
import { objects } from './objects';
|
||||
import { snippets } from './snippets';
|
||||
import { uploads } from './uploads';
|
||||
import { groups } from './groups';
|
||||
import { search } from './search';
|
||||
@ -53,7 +54,7 @@ export const api = {
|
||||
api.req(method, path, data, success, fail, true, options);
|
||||
},
|
||||
|
||||
auth, accounts, users, projects, objects, uploads, groups, search, invitations, root
|
||||
auth, accounts, users, projects, objects, snippets, uploads, groups, search, invitations, root
|
||||
};
|
||||
|
||||
export default api;
|
||||
|
13
web/src/api/snippets.js
Normal file
13
web/src/api/snippets.js
Normal file
@ -0,0 +1,13 @@
|
||||
import api from '.';
|
||||
|
||||
export const snippets = {
|
||||
create(data, success, fail) {
|
||||
api.authenticatedRequest('POST', '/snippets', data, success, fail);
|
||||
},
|
||||
listForUser(success, fail) {
|
||||
api.authenticatedRequest('GET', `/snippets`, null, success, fail);
|
||||
},
|
||||
delete(snippetId, success, fail) {
|
||||
api.authenticatedRequest('DELETE', `/snippets/${snippetId}`, null, success, fail);
|
||||
},
|
||||
};
|
142
web/src/components/includes/SnippetChooser.jsx
Normal file
142
web/src/components/includes/SnippetChooser.jsx
Normal file
@ -0,0 +1,142 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Icon, Divider, Header, Button, Modal, ModalHeader, ModalContent, ModalDescription, ModalActions, Input, Card, Checkbox } from 'semantic-ui-react';
|
||||
import styled from 'styled-components';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import api from '../../api';
|
||||
import actions from '../../actions';
|
||||
import Warp from '../main/projects/objects/Warp';
|
||||
import Weft from '../main/projects/objects/Weft';
|
||||
import { PreviewContainer } from './SnippetSaver';
|
||||
|
||||
export default function SnippetChooser({ type, isOpen, onComplete }) {
|
||||
const [count, setCount] = useState(1);
|
||||
const [includeColours, setIncludeColours] = useState(false);
|
||||
const [selectedSnippet, setSelectedSnippet] = useState(null);
|
||||
const dispatch = useDispatch();
|
||||
const { snippets } = useSelector(state => {
|
||||
return { snippets: state.objects.snippets };
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
api.snippets.listForUser(data => {
|
||||
data?.snippets?.forEach(s => dispatch(actions.objects.receiveSnippet(s)));
|
||||
});
|
||||
}, []);
|
||||
|
||||
const deleteSnippet = (id) => {
|
||||
if (!window.confirm('Are you sure you want to delete this snippet?')) return;
|
||||
api.snippets.delete(id, () => {
|
||||
dispatch(actions.objects.deleteSnippet(id));
|
||||
setSelectedSnippet(null);
|
||||
});
|
||||
}
|
||||
|
||||
const insertSnippet = () => {
|
||||
if (!count) return onComplete();
|
||||
const snippet = selectedSnippet ?
|
||||
{ ...snippets.find(s => s._id === selectedSnippet) }
|
||||
:
|
||||
{
|
||||
type,
|
||||
threading: type === 'warp' ? [{shaft: 0}] : null,
|
||||
treadling: type === 'weft' ? [{treadle: 0}] : null,
|
||||
};
|
||||
if (snippet) {
|
||||
const key = type === 'warp' ? 'threading' : 'treadling';
|
||||
const baseArray = [...snippet[key]];
|
||||
const newArray = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
baseArray.forEach(t => {
|
||||
newArray.push({ ...t, colour: includeColours ? t.colour : undefined });
|
||||
});
|
||||
}
|
||||
snippet[key] = newArray;
|
||||
}
|
||||
onComplete(snippet);
|
||||
}
|
||||
|
||||
const typeSnippets = snippets.filter(s => s.type === type);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
onClose={() => onComplete()}
|
||||
open={isOpen}
|
||||
>
|
||||
<ModalHeader>Insert into the {type}</ModalHeader>
|
||||
<ModalContent>
|
||||
<ModalDescription>
|
||||
<Input
|
||||
label={{ basic: true, content: 'Number of repeats to insert:' }}
|
||||
labelPosition='left'
|
||||
placeholder='Count'
|
||||
value={count}
|
||||
onChange={(e, { value }) => setCount(parseInt(value || 0) || 0)}
|
||||
/>
|
||||
<Divider hidden />
|
||||
<Header size='small'>Choose a blank thread or a snippet</Header>
|
||||
<Card.Group stackable doubling itemsPerRow={4}>
|
||||
<Card
|
||||
raised={!selectedSnippet}
|
||||
color={!selectedSnippet ? 'blue' : null}
|
||||
onClick={() => setSelectedSnippet(null)}
|
||||
>
|
||||
<Card.Content textAlign='center'>
|
||||
{!selectedSnippet && <Icon name='check circle' size='sm' />}
|
||||
<Header size='sm'>Blank thread</Header>
|
||||
</Card.Content>
|
||||
</Card>
|
||||
|
||||
{!typeSnippets.length ? <Card>
|
||||
<Card.Content textAlign='center'>
|
||||
You don't have any {type} snippets yet. Create one by choosing threads in your pattern using the "select" tool.
|
||||
</Card.Content>
|
||||
</Card> : null}
|
||||
|
||||
{typeSnippets.map(s => {
|
||||
const shafts = type === 'warp' ? Math.max(...s.threading?.map(t => t.shaft)) : 0;
|
||||
const treadles = type === 'weft' ? Math.max(...s.treadling?.map(t => t.treadle)) : 0;
|
||||
return (<Card key={s._id}
|
||||
raised={selectedSnippet === s._id}
|
||||
color={selectedSnippet === s._id ? 'blue' : null}
|
||||
onClick={() => setSelectedSnippet(s._id)}
|
||||
>
|
||||
<Card.Content textAlign='center'>
|
||||
{selectedSnippet === s._id && <Icon name='check circle' size='sm' />}
|
||||
<Header size='sm' style={{margin: 0}}>
|
||||
{s.name}
|
||||
</Header>
|
||||
<div style={{overflow: 'scroll'}}>
|
||||
<PreviewContainer type={type} shafts={shafts} treadles={treadles} threading={s.threading} treadling={s.treadling}>
|
||||
{type === 'warp' &&
|
||||
<Warp baseSize={10} warp={{threading: s.threading, shafts}} weft={{treadles, treadling: []}}/>
|
||||
}
|
||||
{type === 'weft' &&
|
||||
<Weft baseSize={10} warp={{shafts, threading: []}} weft={{treadling: s.treadling, treadles}}/>
|
||||
}
|
||||
</PreviewContainer>
|
||||
</div>
|
||||
</Card.Content>
|
||||
<Card.Content extra>
|
||||
<a href='#' onClick={() => deleteSnippet(s._id)}>Delete snippet</a>
|
||||
</Card.Content>
|
||||
</Card>);
|
||||
})}
|
||||
</Card.Group>
|
||||
|
||||
{selectedSnippet && <>
|
||||
<Divider hidden />
|
||||
<Checkbox
|
||||
label='Insert with snippet colours'
|
||||
onChange={(e, data) => setIncludeColours(data.checked)}
|
||||
checked={includeColours}
|
||||
/>
|
||||
</>}
|
||||
</ModalDescription>
|
||||
</ModalContent>
|
||||
<ModalActions>
|
||||
<Button color='black' onClick={() => onComplete()}>Cancel</Button>
|
||||
<Button onClick={insertSnippet} positive>Insert selected thread or snippet</Button>
|
||||
</ModalActions>
|
||||
</Modal>
|
||||
);
|
||||
}
|
78
web/src/components/includes/SnippetSaver.jsx
Normal file
78
web/src/components/includes/SnippetSaver.jsx
Normal file
@ -0,0 +1,78 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Button, Card, Header, Modal, ModalHeader, ModalContent, ModalDescription, ModalActions, Input } from 'semantic-ui-react';
|
||||
import styled from 'styled-components';
|
||||
import api from '../../api';
|
||||
import Warp from '../main/projects/objects/Warp';
|
||||
import Weft from '../main/projects/objects/Weft';
|
||||
|
||||
export const PreviewContainer = styled.div`
|
||||
display: inline-block;
|
||||
margin-top: 10px;
|
||||
position: relative;
|
||||
height: ${p =>
|
||||
(p.type === 'warp' ? p.shafts * 10 :
|
||||
p.type === 'weft' ? p.treadling?.length * 10 : 0) + 20}px;
|
||||
}px;
|
||||
width: ${p =>
|
||||
(p.type === 'warp' ? p.threading?.length * 10 :
|
||||
p.type === 'weft' ? p.treadles * 10 : 0) + 20}px;
|
||||
}px;
|
||||
overflow-y: scroll;
|
||||
`;
|
||||
|
||||
export default function SnippetSaver({ type, threading, treadling, isOpen, onComplete }) {
|
||||
const [name, setName] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
setName('Snippet made on ' + new Date().toLocaleString());
|
||||
}, []);
|
||||
|
||||
const save = () => {
|
||||
api.snippets.create({ name, type, threading, treadling }, newSnippet => {
|
||||
onComplete(newSnippet);
|
||||
});
|
||||
}
|
||||
let shafts = 0;
|
||||
threading?.forEach(thread => {
|
||||
if (thread.shaft > shafts) shafts = thread.shaft;
|
||||
});
|
||||
let treadles = 0;
|
||||
treadling?.forEach(thread => {
|
||||
if (thread.treadle > treadles) treadles = thread.treadle;
|
||||
});
|
||||
|
||||
return (
|
||||
<Modal
|
||||
onClose={() => onComplete()}
|
||||
open={isOpen}
|
||||
>
|
||||
<ModalHeader>Save a {type} snippet</ModalHeader>
|
||||
<ModalContent>
|
||||
<ModalDescription>
|
||||
<p>Snippets are partial warps or wefts that you can re-use later in this and other drafts.</p>
|
||||
<p><strong>Give your snippet a name:</strong></p>
|
||||
<Input placeholder='Type a name...' value={name} onChange={e => setName(e.target.value)} fluid autoFocus />
|
||||
<Card raised>
|
||||
<Card.Content textAlign='center'>
|
||||
<Header size='sm'>Preview</Header>
|
||||
<div style={{overflow: 'scroll'}}>
|
||||
<PreviewContainer type={type} threading={threading} treadling={treadling} shafts={shafts} treadles={treadles}>
|
||||
{type === 'warp' &&
|
||||
<Warp baseSize={10} warp={{threading, shafts}} weft={{treadles: 0, treadling: []}}/>
|
||||
}
|
||||
{type === 'weft' &&
|
||||
<Weft baseSize={10} warp={{shafts: 0, threading: []}} weft={{treadling, treadles}}/>
|
||||
}
|
||||
</PreviewContainer>
|
||||
</div>
|
||||
</Card.Content>
|
||||
</Card>
|
||||
</ModalDescription>
|
||||
</ModalContent>
|
||||
<ModalActions>
|
||||
<Button color='black' onClick={() => onComplete()}>Cancel</Button>
|
||||
<Button onClick={save} positive>Save snippet</Button>
|
||||
</ModalActions>
|
||||
</Modal>
|
||||
);
|
||||
}
|
@ -10,6 +10,8 @@ import Slider from 'rc-slider';
|
||||
import styled from 'styled-components';
|
||||
import HelpLink from '../../../includes/HelpLink';
|
||||
import Tour, { ReRunTour } from '../../../includes/Tour';
|
||||
import SnippetSaver from '../../../includes/SnippetSaver';
|
||||
import SnippetChooser from '../../../includes/SnippetChooser';
|
||||
|
||||
import 'rc-slider/assets/index.css';
|
||||
|
||||
@ -55,6 +57,7 @@ function Tools({ object, pattern, warp, weft, unsaved, saving, baseSize, updateP
|
||||
const [deleteModalOpen, setDeleteModalOpen] = useState(false);
|
||||
const [newColour, setNewColour] = useState('#22194D');
|
||||
const [selectedThreadCount, setSelectedThreadCount] = useState(0);
|
||||
const [creatingSnippet, setCreatingSnippet] = useState(null);
|
||||
const navigate = useNavigate();
|
||||
const dispatch = useDispatch();
|
||||
const { objectId, username, projectPath } = useParams();
|
||||
@ -134,6 +137,33 @@ function Tools({ object, pattern, warp, weft, unsaved, saving, baseSize, updateP
|
||||
newWeft.treadling = newWeft?.treadling?.map(t => ({ ...t, isSelected: undefined }));
|
||||
updatePattern({ warp: newWarp, weft: newWeft });
|
||||
}
|
||||
const saveSnippet = () => {
|
||||
const selectedWarp = warp?.threading?.filter(t => t.isSelected)?.map(t => ({ shaft: t.shaft, colour: t.colour }));
|
||||
const selectedWeft = weft?.treadling?.filter(t => t.isSelected)?.map(t => ({ treadle: t.treadle, colour: t.colour}));
|
||||
if (selectedWarp?.length) setCreatingSnippet({ type: 'warp', threading: selectedWarp, treadling: null });
|
||||
else setCreatingSnippet({ type: 'weft', threading: null, treadling: selectedWeft });
|
||||
}
|
||||
const onSaveSnippet = (snippet) => {
|
||||
setCreatingSnippet(null);
|
||||
dispatch(actions.objects.receiveSnippet(snippet));
|
||||
if (snippet) {
|
||||
toast('📌 Snippet saved');
|
||||
}
|
||||
}
|
||||
const onInsertSnippet = (snippet) => {
|
||||
if (snippet) {
|
||||
const newWarp = Object.assign({}, pattern.warp);
|
||||
const newWeft = Object.assign({}, pattern.weft);
|
||||
if (snippet.type === 'warp') {
|
||||
newWarp.threading.splice(editor.insertPoint, 0, ...snippet.threading);
|
||||
}
|
||||
else {
|
||||
newWeft.treadling.splice(editor.insertPoint, 0, ...snippet.treadling);
|
||||
}
|
||||
updatePattern({ warp: newWarp, weft: newWeft });
|
||||
}
|
||||
dispatch(actions.objects.updateEditor( { insertType: null, inseertPoint: null }));
|
||||
}
|
||||
|
||||
const onZoomChange = zoom => updatePattern({ baseSize: zoom || 10 });
|
||||
|
||||
@ -208,6 +238,7 @@ function Tools({ object, pattern, warp, weft, unsaved, saving, baseSize, updateP
|
||||
<div>
|
||||
<Header>{selectedThreadCount} threads selected</Header>
|
||||
<Button size='small' basic onClick={deselectThreads}>De-select all</Button>
|
||||
<Button size='small' color='teal' onClick={saveSnippet}>Save selection as snippet</Button>
|
||||
<Button size='small' color='orange' onClick={deleteSelectedThreads}>Delete threads</Button>
|
||||
</div>
|
||||
:
|
||||
@ -239,7 +270,7 @@ function Tools({ object, pattern, warp, weft, unsaved, saving, baseSize, updateP
|
||||
<Button.Group size="tiny">
|
||||
<Button className='joyride-pan' data-tooltip="Pan (drag to move) pattern" color={editor.tool === 'pan' && 'blue'} size="tiny" icon onClick={() => enableTool('pan')}><Icon name="move" /></Button>
|
||||
<Button data-tooltip="Select threads" color={editor.tool === 'select' && 'blue'} size="tiny" icon onClick={() => enableTool('select')}><Icon name="i cursor" /></Button>
|
||||
<Button data-tooltip="Insert threads" color={editor.tool === 'insert' && 'blue'} size="tiny" icon onClick={() => enableTool('insert')}><Icon name="plus" /></Button>
|
||||
<Button data-tooltip="Insert threads or snippets" color={editor.tool === 'insert' && 'blue'} size="tiny" icon onClick={() => enableTool('insert')}><Icon name="plus" /></Button>
|
||||
<Button className='joyride-colour' data-tooltip="Apply thread colour" color={editor.tool === 'colour' && 'blue'} size="tiny" icon onClick={() => enableTool('colour')}><Icon name="paint brush" /></Button>
|
||||
<Button data-tooltip="Erase threads" color={editor.tool === 'eraser' && 'blue'} size="tiny" icon onClick={() => enableTool('eraser')}><Icon name="eraser" /></Button>
|
||||
<Button className='joyride-straight' data-tooltip="Straight draw" color={editor.tool === 'straight' && 'blue'} size="tiny" icon onClick={() => enableTool('straight')}>/ /</Button>
|
||||
@ -254,7 +285,7 @@ function Tools({ object, pattern, warp, weft, unsaved, saving, baseSize, updateP
|
||||
</div>
|
||||
<Button.Group size="tiny">
|
||||
<Popup hoverable on="click"
|
||||
trigger={<Button style={{marginLeft: 5}} size="tiny" icon="tint" style={{color: utils.rgb(editor.colour)}} />}
|
||||
trigger={<Button size="tiny" icon="tint" style={{marginLeft: 5, color: utils.rgb(editor.colour)}} />}
|
||||
content={<div style={{width: 150}}>
|
||||
{pattern.colours && pattern.colours.map(colour =>
|
||||
<ColourSquare key={colour} colour={utils.rgb(colour)} onClick={() => setColour(colour)} />
|
||||
@ -332,26 +363,6 @@ function Tools({ object, pattern, warp, weft, unsaved, saving, baseSize, updateP
|
||||
onConfirm={deleteObject}
|
||||
/>
|
||||
|
||||
{/*}
|
||||
<Dropdown icon={null} direction='left' disabled={unsaved}
|
||||
trigger={<Button color='blue' size='tiny' icon='download' style={{marginLeft: 5}} />}
|
||||
>
|
||||
<Dropdown.Menu>
|
||||
{object.previewUrl &&
|
||||
<Dropdown.Item onClick={e => utils.downloadDrawdownImage(object)} content='Download drawdown as an image' icon='file outline' />
|
||||
}
|
||||
{(utils.canEditProject(user, project) || project.openSource) &&
|
||||
<>
|
||||
{object.patternImage &&
|
||||
<Dropdown.Item icon='file outline' content='Download complete pattern as an image' onClick={e => utils.downloadPatternImage(object)}/>
|
||||
}
|
||||
<Dropdown.Divider />
|
||||
<Dropdown.Item onClick={e => utils.downloadWif(object)} content="Download pattern in WIF format" icon="text file" />
|
||||
</>
|
||||
}
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>*/}
|
||||
|
||||
<Dropdown style={{marginLeft: 5}} icon={null} direction='left'
|
||||
trigger={<Button basic size='tiny' icon='help' />}
|
||||
>
|
||||
@ -370,6 +381,10 @@ function Tools({ object, pattern, warp, weft, unsaved, saving, baseSize, updateP
|
||||
</div>
|
||||
</div>
|
||||
</Segment>
|
||||
{creatingSnippet &&
|
||||
<SnippetSaver type={creatingSnippet.type} threading={creatingSnippet.threading} treadling={creatingSnippet.treadling} isOpen={!!creatingSnippet} onComplete={onSaveSnippet} />
|
||||
}
|
||||
<SnippetChooser type={editor?.insertType} isOpen={!!editor?.insertType} onComplete={onInsertSnippet} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1,7 +1,8 @@
|
||||
import React, { useEffect, useState, useRef } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import utils from '../../../../utils/utils.js';
|
||||
import actions from '../../../../actions/index.js';
|
||||
|
||||
const WarpContainer = styled.div`
|
||||
top:0px;
|
||||
@ -50,6 +51,7 @@ function Warp({ baseSize, cellStyle, warp, weft, updatePattern }) {
|
||||
const { tool, colour } = editor;
|
||||
const warpRef = useRef(null);
|
||||
const colourwayRef = useRef(null);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
useEffect(() => paintDrawdown());
|
||||
useEffect(() => {
|
||||
@ -218,13 +220,7 @@ function Warp({ baseSize, cellStyle, warp, weft, updatePattern }) {
|
||||
updatePattern({ warp: newWarp });
|
||||
}
|
||||
if (editor.tool === 'insert') {
|
||||
const number = parseInt(prompt('Enter a number of threads to insert before this point.'));
|
||||
if (number && number > 0) {
|
||||
const newThreads = [...new Array(number)].map(() => ({ shaft: 0 }));
|
||||
const newWarp = Object.assign({}, warp);
|
||||
newWarp.threading?.splice(thread, 0, ...newThreads);
|
||||
updatePattern({ warp: newWarp });
|
||||
}
|
||||
dispatch(actions.objects.updateEditor({ insertType: 'warp', insertPoint: thread }));
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -1,7 +1,8 @@
|
||||
import React, { useEffect, useState, useRef } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import utils from '../../../../utils/utils.js';
|
||||
import actions from '../../../../actions';
|
||||
|
||||
const WeftContainer = styled.div`
|
||||
position: absolute;
|
||||
@ -50,6 +51,7 @@ function Weft({ cellStyle, warp, weft, baseSize, updatePattern }) {
|
||||
const { tool, colour } = editor;
|
||||
const weftRef = useRef(null);
|
||||
const colourwayRef = useRef(null);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
useEffect(() => paintDrawdown());
|
||||
useEffect(() => {
|
||||
@ -220,13 +222,7 @@ function Weft({ cellStyle, warp, weft, baseSize, updatePattern }) {
|
||||
updatePattern({ weft: newWeft });
|
||||
}
|
||||
if (editor.tool === 'insert') {
|
||||
const number = parseInt(prompt('Enter a number of threads to insert above this point.'));
|
||||
if (number && number > 0) {
|
||||
const newThreads = [...new Array(number)].map(() => ({ treadle: 0 }));
|
||||
const newWeft = Object.assign({}, weft);
|
||||
newWeft.treadling?.splice(thread - 1, 0, ...newThreads);
|
||||
updatePattern({ weft: newWeft });
|
||||
}
|
||||
dispatch(actions.objects.updateEditor({ insertType: 'weft', insertPoint: thread - 1 }));
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -8,6 +8,7 @@ const initialState = {
|
||||
comments: [],
|
||||
selected: null,
|
||||
editor: { tool: 'straight', colour: null, view: 'interlacement', autoExtend: true },
|
||||
snippets: [],
|
||||
};
|
||||
|
||||
function objects(state = initialState, action) {
|
||||
@ -76,6 +77,22 @@ function objects(state = initialState, action) {
|
||||
case actions.objects.UPDATE_EDITOR:
|
||||
const editor = Object.assign({}, state.editor, action.editor);
|
||||
return Object.assign({}, state, { editor });
|
||||
case actions.objects.RECEIVE_SNIPPET: {
|
||||
const snippets = [];
|
||||
let found = false;
|
||||
state.snippets.forEach((snippet) => {
|
||||
if (snippet._id === action.snippet._id) {
|
||||
snippet = Object.assign({}, action.snippet);
|
||||
found = true;
|
||||
}
|
||||
snippets.push(snippet);
|
||||
});
|
||||
if (!found) snippets.splice(0, 0, action.snippet);
|
||||
return Object.assign({}, state, { loading: false, snippets });
|
||||
}
|
||||
case actions.objects.DELETE_SNIPPET: {
|
||||
return Object.assign({}, state, { snippets: state.snippets.filter(e => e._id !== action.snippetId) });
|
||||
}
|
||||
|
||||
case actions.objects.RECEIVE_COMMENT: {
|
||||
let found = false;
|
||||
|
Loading…
Reference in New Issue
Block a user