add quilljs, create channel for syncing documents
This commit is contained in:
parent
7de2bad456
commit
52a7a64d23
|
@ -0,0 +1,210 @@
|
||||||
|
%{
|
||||||
|
#
|
||||||
|
# You can have as many configs as you like in the `configs:` field.
|
||||||
|
configs: [
|
||||||
|
%{
|
||||||
|
#
|
||||||
|
# Run any config using `mix credo -C <name>`. If no config name is given
|
||||||
|
# "default" is used.
|
||||||
|
#
|
||||||
|
name: "default",
|
||||||
|
#
|
||||||
|
# These are the files included in the analysis:
|
||||||
|
files: %{
|
||||||
|
#
|
||||||
|
# You can give explicit globs or simply directories.
|
||||||
|
# In the latter case `**/*.{ex,exs}` will be used.
|
||||||
|
#
|
||||||
|
included: [
|
||||||
|
"lib/",
|
||||||
|
"src/",
|
||||||
|
"test/",
|
||||||
|
"web/",
|
||||||
|
"apps/*/lib/",
|
||||||
|
"apps/*/src/",
|
||||||
|
"apps/*/test/",
|
||||||
|
"apps/*/web/"
|
||||||
|
],
|
||||||
|
excluded: [~r"/_build/", ~r"/deps/", ~r"/node_modules/"]
|
||||||
|
},
|
||||||
|
#
|
||||||
|
# Load and configure plugins here:
|
||||||
|
#
|
||||||
|
plugins: [],
|
||||||
|
#
|
||||||
|
# If you create your own checks, you must specify the source files for
|
||||||
|
# them here, so they can be loaded by Credo before running the analysis.
|
||||||
|
#
|
||||||
|
requires: [],
|
||||||
|
#
|
||||||
|
# If you want to enforce a style guide and need a more traditional linting
|
||||||
|
# experience, you can change `strict` to `true` below:
|
||||||
|
#
|
||||||
|
strict: false,
|
||||||
|
#
|
||||||
|
# To modify the timeout for parsing files, change this value:
|
||||||
|
#
|
||||||
|
parse_timeout: 5000,
|
||||||
|
#
|
||||||
|
# If you want to use uncolored output by default, you can change `color`
|
||||||
|
# to `false` below:
|
||||||
|
#
|
||||||
|
color: true,
|
||||||
|
#
|
||||||
|
# You can customize the parameters of any check by adding a second element
|
||||||
|
# to the tuple.
|
||||||
|
#
|
||||||
|
# To disable a check put `false` as second element:
|
||||||
|
#
|
||||||
|
# {Credo.Check.Design.DuplicatedCode, false}
|
||||||
|
#
|
||||||
|
checks: %{
|
||||||
|
enabled: [
|
||||||
|
#
|
||||||
|
## Consistency Checks
|
||||||
|
#
|
||||||
|
{Credo.Check.Consistency.ExceptionNames, []},
|
||||||
|
{Credo.Check.Consistency.LineEndings, []},
|
||||||
|
{Credo.Check.Consistency.ParameterPatternMatching, []},
|
||||||
|
{Credo.Check.Consistency.SpaceAroundOperators, []},
|
||||||
|
{Credo.Check.Consistency.SpaceInParentheses, []},
|
||||||
|
{Credo.Check.Consistency.TabsOrSpaces, []},
|
||||||
|
|
||||||
|
#
|
||||||
|
## Design Checks
|
||||||
|
#
|
||||||
|
# You can customize the priority of any check
|
||||||
|
# Priority values are: `low, normal, high, higher`
|
||||||
|
#
|
||||||
|
{Credo.Check.Design.AliasUsage,
|
||||||
|
[priority: :low, if_nested_deeper_than: 2, if_called_more_often_than: 0]},
|
||||||
|
{Credo.Check.Design.TagFIXME, []},
|
||||||
|
# You can also customize the exit_status of each check.
|
||||||
|
# If you don't want TODO comments to cause `mix credo` to fail, just
|
||||||
|
# set this value to 0 (zero).
|
||||||
|
#
|
||||||
|
{Credo.Check.Design.TagTODO, [exit_status: 2]},
|
||||||
|
|
||||||
|
#
|
||||||
|
## Readability Checks
|
||||||
|
#
|
||||||
|
{Credo.Check.Readability.AliasOrder, []},
|
||||||
|
{Credo.Check.Readability.FunctionNames, []},
|
||||||
|
{Credo.Check.Readability.LargeNumbers, []},
|
||||||
|
{Credo.Check.Readability.MaxLineLength, [priority: :low, max_length: 120]},
|
||||||
|
{Credo.Check.Readability.ModuleAttributeNames, []},
|
||||||
|
{Credo.Check.Readability.ModuleDoc, []},
|
||||||
|
{Credo.Check.Readability.ModuleNames, []},
|
||||||
|
{Credo.Check.Readability.ParenthesesInCondition, []},
|
||||||
|
{Credo.Check.Readability.ParenthesesOnZeroArityDefs, []},
|
||||||
|
{Credo.Check.Readability.PipeIntoAnonymousFunctions, []},
|
||||||
|
{Credo.Check.Readability.PredicateFunctionNames, []},
|
||||||
|
{Credo.Check.Readability.PreferImplicitTry, []},
|
||||||
|
{Credo.Check.Readability.RedundantBlankLines, []},
|
||||||
|
{Credo.Check.Readability.Semicolons, []},
|
||||||
|
{Credo.Check.Readability.SpaceAfterCommas, []},
|
||||||
|
{Credo.Check.Readability.StringSigils, []},
|
||||||
|
{Credo.Check.Readability.TrailingBlankLine, []},
|
||||||
|
{Credo.Check.Readability.TrailingWhiteSpace, []},
|
||||||
|
{Credo.Check.Readability.UnnecessaryAliasExpansion, []},
|
||||||
|
{Credo.Check.Readability.VariableNames, []},
|
||||||
|
{Credo.Check.Readability.WithSingleClause, []},
|
||||||
|
|
||||||
|
#
|
||||||
|
## Refactoring Opportunities
|
||||||
|
#
|
||||||
|
{Credo.Check.Refactor.Apply, []},
|
||||||
|
{Credo.Check.Refactor.CondStatements, []},
|
||||||
|
{Credo.Check.Refactor.CyclomaticComplexity, []},
|
||||||
|
{Credo.Check.Refactor.FilterCount, []},
|
||||||
|
{Credo.Check.Refactor.FilterFilter, []},
|
||||||
|
{Credo.Check.Refactor.FunctionArity, []},
|
||||||
|
{Credo.Check.Refactor.LongQuoteBlocks, []},
|
||||||
|
{Credo.Check.Refactor.MapJoin, []},
|
||||||
|
{Credo.Check.Refactor.MatchInCondition, []},
|
||||||
|
{Credo.Check.Refactor.NegatedConditionsInUnless, []},
|
||||||
|
{Credo.Check.Refactor.NegatedConditionsWithElse, []},
|
||||||
|
{Credo.Check.Refactor.Nesting, []},
|
||||||
|
{Credo.Check.Refactor.RedundantWithClauseResult, []},
|
||||||
|
{Credo.Check.Refactor.RejectReject, []},
|
||||||
|
{Credo.Check.Refactor.UnlessWithElse, []},
|
||||||
|
{Credo.Check.Refactor.WithClauses, []},
|
||||||
|
|
||||||
|
#
|
||||||
|
## Warnings
|
||||||
|
#
|
||||||
|
{Credo.Check.Warning.ApplicationConfigInModuleAttribute, []},
|
||||||
|
{Credo.Check.Warning.BoolOperationOnSameValues, []},
|
||||||
|
{Credo.Check.Warning.Dbg, []},
|
||||||
|
{Credo.Check.Warning.ExpensiveEmptyEnumCheck, []},
|
||||||
|
{Credo.Check.Warning.IExPry, []},
|
||||||
|
{Credo.Check.Warning.IoInspect, []},
|
||||||
|
{Credo.Check.Warning.MissedMetadataKeyInLoggerConfig, []},
|
||||||
|
{Credo.Check.Warning.OperationOnSameValues, []},
|
||||||
|
{Credo.Check.Warning.OperationWithConstantResult, []},
|
||||||
|
{Credo.Check.Warning.RaiseInsideRescue, []},
|
||||||
|
{Credo.Check.Warning.SpecWithStruct, []},
|
||||||
|
{Credo.Check.Warning.UnsafeExec, []},
|
||||||
|
{Credo.Check.Warning.UnusedEnumOperation, []},
|
||||||
|
{Credo.Check.Warning.UnusedFileOperation, []},
|
||||||
|
{Credo.Check.Warning.UnusedKeywordOperation, []},
|
||||||
|
{Credo.Check.Warning.UnusedListOperation, []},
|
||||||
|
{Credo.Check.Warning.UnusedPathOperation, []},
|
||||||
|
{Credo.Check.Warning.UnusedRegexOperation, []},
|
||||||
|
{Credo.Check.Warning.UnusedStringOperation, []},
|
||||||
|
{Credo.Check.Warning.UnusedTupleOperation, []},
|
||||||
|
{Credo.Check.Warning.WrongTestFileExtension, []}
|
||||||
|
],
|
||||||
|
disabled: [
|
||||||
|
#
|
||||||
|
# Checks scheduled for next check update (opt-in for now, just replace `false` with `[]`)
|
||||||
|
|
||||||
|
#
|
||||||
|
# Controversial and experimental checks (opt-in, just move the check to `:enabled`
|
||||||
|
# and be sure to use `mix credo --strict` to see low priority checks)
|
||||||
|
#
|
||||||
|
{Credo.Check.Consistency.MultiAliasImportRequireUse, []},
|
||||||
|
{Credo.Check.Consistency.UnusedVariableNames, []},
|
||||||
|
{Credo.Check.Design.DuplicatedCode, []},
|
||||||
|
{Credo.Check.Design.SkipTestWithoutComment, []},
|
||||||
|
{Credo.Check.Readability.AliasAs, []},
|
||||||
|
{Credo.Check.Readability.BlockPipe, []},
|
||||||
|
{Credo.Check.Readability.ImplTrue, []},
|
||||||
|
{Credo.Check.Readability.MultiAlias, []},
|
||||||
|
{Credo.Check.Readability.NestedFunctionCalls, []},
|
||||||
|
{Credo.Check.Readability.OneArityFunctionInPipe, []},
|
||||||
|
{Credo.Check.Readability.OnePipePerLine, []},
|
||||||
|
{Credo.Check.Readability.SeparateAliasRequire, []},
|
||||||
|
{Credo.Check.Readability.SingleFunctionToBlockPipe, []},
|
||||||
|
{Credo.Check.Readability.SinglePipe, []},
|
||||||
|
{Credo.Check.Readability.Specs, []},
|
||||||
|
{Credo.Check.Readability.StrictModuleLayout, []},
|
||||||
|
{Credo.Check.Readability.WithCustomTaggedTuple, []},
|
||||||
|
{Credo.Check.Refactor.ABCSize, []},
|
||||||
|
{Credo.Check.Refactor.AppendSingleItem, []},
|
||||||
|
{Credo.Check.Refactor.DoubleBooleanNegation, []},
|
||||||
|
{Credo.Check.Refactor.FilterReject, []},
|
||||||
|
{Credo.Check.Refactor.IoPuts, []},
|
||||||
|
{Credo.Check.Refactor.MapMap, []},
|
||||||
|
{Credo.Check.Refactor.ModuleDependencies, []},
|
||||||
|
{Credo.Check.Refactor.NegatedIsNil, []},
|
||||||
|
{Credo.Check.Refactor.PassAsyncInTestCases, []},
|
||||||
|
{Credo.Check.Refactor.PipeChainStart, []},
|
||||||
|
{Credo.Check.Refactor.RejectFilter, []},
|
||||||
|
{Credo.Check.Refactor.VariableRebinding, []},
|
||||||
|
{Credo.Check.Warning.LazyLogging, []},
|
||||||
|
{Credo.Check.Warning.LeakyEnvironment, []},
|
||||||
|
{Credo.Check.Warning.MapGetUnsafePass, []},
|
||||||
|
{Credo.Check.Warning.MixEnv, []},
|
||||||
|
{Credo.Check.Warning.UnsafeToAtom, []}
|
||||||
|
|
||||||
|
# {Credo.Check.Refactor.MapInto, []},
|
||||||
|
|
||||||
|
#
|
||||||
|
# Custom checks can be created using `mix credo.gen.check`.
|
||||||
|
#
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
// If you want to use Phoenix channels, run `mix help phx.gen.channel`
|
// If you want to use Phoenix channels, run `mix help phx.gen.channel`
|
||||||
// to get started and then uncomment the line below.
|
// to get started and then uncomment the line below.
|
||||||
// import "./user_socket.js"
|
import "./user_socket.js"
|
||||||
|
|
||||||
// You can include dependencies in two ways.
|
// You can include dependencies in two ways.
|
||||||
//
|
//
|
||||||
|
@ -21,9 +21,14 @@ import "phoenix_html"
|
||||||
import {Socket} from "phoenix"
|
import {Socket} from "phoenix"
|
||||||
import {LiveSocket} from "phoenix_live_view"
|
import {LiveSocket} from "phoenix_live_view"
|
||||||
import topbar from "../vendor/topbar"
|
import topbar from "../vendor/topbar"
|
||||||
|
import { TextEditor } from './hooks/textEditHook'
|
||||||
|
|
||||||
|
|
||||||
let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
|
let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
|
||||||
let liveSocket = new LiveSocket("/live", Socket, {params: {_csrf_token: csrfToken}})
|
|
||||||
|
let Hooks = {}
|
||||||
|
Hooks.TextEditor = TextEditor
|
||||||
|
let liveSocket = new LiveSocket("/live", Socket, {hooks: Hooks, params: {_csrf_token: csrfToken}})
|
||||||
|
|
||||||
// Show progress bar on live navigation and form submits
|
// Show progress bar on live navigation and form submits
|
||||||
topbar.config({barColors: {0: "#29d"}, shadowColor: "rgba(0, 0, 0, .3)"})
|
topbar.config({barColors: {0: "#29d"}, shadowColor: "rgba(0, 0, 0, .3)"})
|
||||||
|
|
|
@ -0,0 +1,43 @@
|
||||||
|
// https://elixirforum.com/t/how-to-connect-quill-with-phoenix/46004
|
||||||
|
import Quill from 'quill';
|
||||||
|
import socket from '../user_socket';
|
||||||
|
|
||||||
|
export let TextEditor = {
|
||||||
|
mounted() {
|
||||||
|
const padId = this.el.dataset.padId;
|
||||||
|
this.clientId;
|
||||||
|
|
||||||
|
this.quill = new Quill(this.el, {
|
||||||
|
theme: 'snow'
|
||||||
|
});
|
||||||
|
|
||||||
|
let channel = socket.channel(`pad:${padId}`, {});
|
||||||
|
|
||||||
|
channel.join()
|
||||||
|
.receive("ok", ({uuid, contents}) => {
|
||||||
|
this.clientId = uuid;
|
||||||
|
this.quill.setContents(contents);
|
||||||
|
})
|
||||||
|
// TODO: Probably need to show an alert that they couldn't join
|
||||||
|
.receive("error", resp => { console.log("Unable to join", resp) });
|
||||||
|
|
||||||
|
channel.on("update", ({change, client_id}) => {
|
||||||
|
if(client_id === this.clientId) return;
|
||||||
|
let range = this.quill.getSelection();
|
||||||
|
this.quill.updateContents(change);
|
||||||
|
range && this.quill.setSelection(range.index, range.length);
|
||||||
|
})
|
||||||
|
|
||||||
|
this.quill.on('text-change', (delta, oldDelta, source) => {
|
||||||
|
if (delta == oldDelta) return;
|
||||||
|
if (source == 'api') {
|
||||||
|
console.log("An API call triggered this change.");
|
||||||
|
} else if (source == 'user') {
|
||||||
|
channel.push('update', {change: delta, client_id: this.clientId});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
updated(){
|
||||||
|
console.log('U');
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
import {Socket} from "phoenix"
|
||||||
|
|
||||||
|
let socket = new Socket("/socket", {params: {token: window.userToken}})
|
||||||
|
socket.connect()
|
||||||
|
|
||||||
|
export default socket;
|
|
@ -0,0 +1,365 @@
|
||||||
|
{
|
||||||
|
"name": "assets",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"dependencies": {
|
||||||
|
"quill": "^1.3.7",
|
||||||
|
"quill-delta": "^5.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/call-bind": {
|
||||||
|
"version": "1.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz",
|
||||||
|
"integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"function-bind": "^1.1.2",
|
||||||
|
"get-intrinsic": "^1.2.1",
|
||||||
|
"set-function-length": "^1.1.1"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/clone": {
|
||||||
|
"version": "2.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz",
|
||||||
|
"integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/deep-equal": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-5tdhKF6DbU7iIzrIOa1AOUt39ZRm13cmL1cGEh//aqR8x9+tNfbywRf0n5FD/18OKMdo7DNEtrX2t22ZAkI+eg==",
|
||||||
|
"dependencies": {
|
||||||
|
"is-arguments": "^1.1.1",
|
||||||
|
"is-date-object": "^1.0.5",
|
||||||
|
"is-regex": "^1.1.4",
|
||||||
|
"object-is": "^1.1.5",
|
||||||
|
"object-keys": "^1.1.1",
|
||||||
|
"regexp.prototype.flags": "^1.5.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/define-data-property": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"get-intrinsic": "^1.2.1",
|
||||||
|
"gopd": "^1.0.1",
|
||||||
|
"has-property-descriptors": "^1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/define-properties": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==",
|
||||||
|
"dependencies": {
|
||||||
|
"define-data-property": "^1.0.1",
|
||||||
|
"has-property-descriptors": "^1.0.0",
|
||||||
|
"object-keys": "^1.1.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/eventemitter3": {
|
||||||
|
"version": "2.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-2.0.3.tgz",
|
||||||
|
"integrity": "sha512-jLN68Dx5kyFHaePoXWPsCGW5qdyZQtLYHkxkg02/Mz6g0kYpDx4FyP6XfArhQdlOC4b8Mv+EMxPo/8La7Tzghg=="
|
||||||
|
},
|
||||||
|
"node_modules/extend": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="
|
||||||
|
},
|
||||||
|
"node_modules/fast-diff": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw=="
|
||||||
|
},
|
||||||
|
"node_modules/function-bind": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/functions-have-names": {
|
||||||
|
"version": "1.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz",
|
||||||
|
"integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/get-intrinsic": {
|
||||||
|
"version": "1.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz",
|
||||||
|
"integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==",
|
||||||
|
"dependencies": {
|
||||||
|
"function-bind": "^1.1.2",
|
||||||
|
"has-proto": "^1.0.1",
|
||||||
|
"has-symbols": "^1.0.3",
|
||||||
|
"hasown": "^2.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/gopd": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
|
||||||
|
"dependencies": {
|
||||||
|
"get-intrinsic": "^1.1.3"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/has-property-descriptors": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==",
|
||||||
|
"dependencies": {
|
||||||
|
"get-intrinsic": "^1.2.2"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/has-proto": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/has-symbols": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/has-tostringtag": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"has-symbols": "^1.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/hasown": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==",
|
||||||
|
"dependencies": {
|
||||||
|
"function-bind": "^1.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/is-arguments": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bind": "^1.0.2",
|
||||||
|
"has-tostringtag": "^1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/is-date-object": {
|
||||||
|
"version": "1.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz",
|
||||||
|
"integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"has-tostringtag": "^1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/is-regex": {
|
||||||
|
"version": "1.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz",
|
||||||
|
"integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bind": "^1.0.2",
|
||||||
|
"has-tostringtag": "^1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lodash.clonedeep": {
|
||||||
|
"version": "4.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
|
||||||
|
"integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ=="
|
||||||
|
},
|
||||||
|
"node_modules/lodash.isequal": {
|
||||||
|
"version": "4.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
|
||||||
|
"integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ=="
|
||||||
|
},
|
||||||
|
"node_modules/object-is": {
|
||||||
|
"version": "1.1.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz",
|
||||||
|
"integrity": "sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bind": "^1.0.2",
|
||||||
|
"define-properties": "^1.1.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/object-keys": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/parchment": {
|
||||||
|
"version": "1.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/parchment/-/parchment-1.1.4.tgz",
|
||||||
|
"integrity": "sha512-J5FBQt/pM2inLzg4hEWmzQx/8h8D0CiDxaG3vyp9rKrQRSDgBlhjdP5jQGgosEajXPSQouXGHOmVdgo7QmJuOg=="
|
||||||
|
},
|
||||||
|
"node_modules/quill": {
|
||||||
|
"version": "1.3.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/quill/-/quill-1.3.7.tgz",
|
||||||
|
"integrity": "sha512-hG/DVzh/TiknWtE6QmWAF/pxoZKYxfe3J/d/+ShUWkDvvkZQVTPeVmUJVu1uE6DDooC4fWTiCLh84ul89oNz5g==",
|
||||||
|
"dependencies": {
|
||||||
|
"clone": "^2.1.1",
|
||||||
|
"deep-equal": "^1.0.1",
|
||||||
|
"eventemitter3": "^2.0.3",
|
||||||
|
"extend": "^3.0.2",
|
||||||
|
"parchment": "^1.1.4",
|
||||||
|
"quill-delta": "^3.6.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/quill-delta": {
|
||||||
|
"version": "5.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/quill-delta/-/quill-delta-5.1.0.tgz",
|
||||||
|
"integrity": "sha512-X74oCeRI4/p0ucjb5Ma8adTXd9Scumz367kkMK5V/IatcX6A0vlgLgKbzXWy5nZmCGeNJm2oQX0d2Eqj+ZIlCA==",
|
||||||
|
"dependencies": {
|
||||||
|
"fast-diff": "^1.3.0",
|
||||||
|
"lodash.clonedeep": "^4.5.0",
|
||||||
|
"lodash.isequal": "^4.5.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/quill/node_modules/fast-diff": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-KaJUt+M9t1qaIteSvjc6P3RbMdXsNhK61GRftR6SNxqmhthcd9MGIi4T+o0jD8LUSpSnSKXE20nLtJ3fOHxQig=="
|
||||||
|
},
|
||||||
|
"node_modules/quill/node_modules/quill-delta": {
|
||||||
|
"version": "3.6.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/quill-delta/-/quill-delta-3.6.3.tgz",
|
||||||
|
"integrity": "sha512-wdIGBlcX13tCHOXGMVnnTVFtGRLoP0imqxM696fIPwIf5ODIYUHIvHbZcyvGlZFiFhK5XzDC2lpjbxRhnM05Tg==",
|
||||||
|
"dependencies": {
|
||||||
|
"deep-equal": "^1.0.1",
|
||||||
|
"extend": "^3.0.2",
|
||||||
|
"fast-diff": "1.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/regexp.prototype.flags": {
|
||||||
|
"version": "1.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz",
|
||||||
|
"integrity": "sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bind": "^1.0.2",
|
||||||
|
"define-properties": "^1.2.0",
|
||||||
|
"set-function-name": "^2.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/set-function-length": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"define-data-property": "^1.1.1",
|
||||||
|
"get-intrinsic": "^1.2.1",
|
||||||
|
"gopd": "^1.0.1",
|
||||||
|
"has-property-descriptors": "^1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/set-function-name": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==",
|
||||||
|
"dependencies": {
|
||||||
|
"define-data-property": "^1.0.1",
|
||||||
|
"functions-have-names": "^1.2.3",
|
||||||
|
"has-property-descriptors": "^1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"dependencies": {
|
||||||
|
"quill": "^1.3.7",
|
||||||
|
"quill-delta": "^5.1.0"
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,6 +6,7 @@ const fs = require("fs")
|
||||||
const path = require("path")
|
const path = require("path")
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
darkMode: 'media',
|
||||||
content: [
|
content: [
|
||||||
"./js/**/*.js",
|
"./js/**/*.js",
|
||||||
"../lib/poex_web.ex",
|
"../lib/poex_web.ex",
|
||||||
|
|
|
@ -1,4 +1,8 @@
|
||||||
defmodule Poex.Accounts.User do
|
defmodule Poex.Accounts.User do
|
||||||
|
@moduledoc """
|
||||||
|
A user changeset for registration.
|
||||||
|
"""
|
||||||
|
|
||||||
use Ecto.Schema
|
use Ecto.Schema
|
||||||
import Ecto.Changeset
|
import Ecto.Changeset
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,7 @@
|
||||||
defmodule Poex.Accounts.UserNotifier do
|
defmodule Poex.Accounts.UserNotifier do
|
||||||
|
@moduledoc """
|
||||||
|
Handle emails via Swoosh
|
||||||
|
"""
|
||||||
import Swoosh.Email
|
import Swoosh.Email
|
||||||
|
|
||||||
alias Poex.Mailer
|
alias Poex.Mailer
|
||||||
|
|
|
@ -1,4 +1,9 @@
|
||||||
defmodule Poex.Accounts.UserToken do
|
defmodule Poex.Accounts.UserToken do
|
||||||
|
@moduledoc """
|
||||||
|
Generates a token that will be stored in a signed place,
|
||||||
|
such as session or cookie. As they are signed, those
|
||||||
|
tokens do not need to be hashed.
|
||||||
|
"""
|
||||||
use Ecto.Schema
|
use Ecto.Schema
|
||||||
import Ecto.Query
|
import Ecto.Query
|
||||||
alias Poex.Accounts.UserToken
|
alias Poex.Accounts.UserToken
|
||||||
|
|
|
@ -12,8 +12,10 @@ defmodule Poex.Application do
|
||||||
Poex.Repo,
|
Poex.Repo,
|
||||||
{DNSCluster, query: Application.get_env(:poex, :dns_cluster_query) || :ignore},
|
{DNSCluster, query: Application.get_env(:poex, :dns_cluster_query) || :ignore},
|
||||||
{Phoenix.PubSub, name: Poex.PubSub},
|
{Phoenix.PubSub, name: Poex.PubSub},
|
||||||
|
{Registry, keys: :unique, name: Poex.Pads.DocumentRegistry},
|
||||||
# Start the Finch HTTP client for sending emails
|
# Start the Finch HTTP client for sending emails
|
||||||
{Finch, name: Poex.Finch},
|
{Finch, name: Poex.Finch},
|
||||||
|
Poex.Pads.DocumentDynamicSupervisor,
|
||||||
# Start a worker by calling: Poex.Worker.start_link(arg)
|
# Start a worker by calling: Poex.Worker.start_link(arg)
|
||||||
# {Poex.Worker, arg},
|
# {Poex.Worker, arg},
|
||||||
# Start to serve requests, typically the last entry
|
# Start to serve requests, typically the last entry
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
defmodule Poex.Mailer do
|
defmodule Poex.Mailer do
|
||||||
|
@moduledoc false
|
||||||
use Swoosh.Mailer, otp_app: :poex
|
use Swoosh.Mailer, otp_app: :poex
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
defmodule Poex.Pads do
|
||||||
|
@moduledoc """
|
||||||
|
The Accounts context.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import Ecto.Query, warn: false
|
||||||
|
alias Poex.Repo
|
||||||
|
alias Poex.Pads.Document
|
||||||
|
|
||||||
|
def get_pad_document(id), do: Repo.get(Document, id)
|
||||||
|
|
||||||
|
def update_pad_document(%Document{} = document, attrs) do
|
||||||
|
document
|
||||||
|
|> Document.changeset(attrs)
|
||||||
|
|> Repo.update()
|
||||||
|
end
|
||||||
|
|
||||||
|
def update_pad_document(id, attrs) do
|
||||||
|
Repo.get(Document, id)
|
||||||
|
|> case do
|
||||||
|
nil ->
|
||||||
|
{:error, :not_found}
|
||||||
|
|
||||||
|
document ->
|
||||||
|
document
|
||||||
|
|> Document.changeset(attrs)
|
||||||
|
|> Repo.update()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,24 @@
|
||||||
|
defmodule Poex.Pads.Document do
|
||||||
|
@moduledoc """
|
||||||
|
Schema for PadDocuments created by users
|
||||||
|
"""
|
||||||
|
|
||||||
|
use Ecto.Schema
|
||||||
|
import Ecto.Changeset
|
||||||
|
|
||||||
|
@primary_key {:id, Ecto.UUID, autogenerate: true}
|
||||||
|
schema "pad_documents" do
|
||||||
|
field :title, :string
|
||||||
|
field :state, :map, default: %{ops: []}
|
||||||
|
has_many :operations, Poex.Pads.Operation
|
||||||
|
|
||||||
|
timestamps(type: :utc_datetime)
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc false
|
||||||
|
def changeset(document, attrs) do
|
||||||
|
document
|
||||||
|
|> cast(attrs, [:title, :state])
|
||||||
|
|> cast_assoc(:operations)
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,112 @@
|
||||||
|
defmodule Poex.Pads.DocumentServer do
|
||||||
|
use GenServer
|
||||||
|
alias Poex.Utils.DeltaUtils
|
||||||
|
|
||||||
|
@initial_state %{
|
||||||
|
# Number of changes made to the document so far
|
||||||
|
version: 0,
|
||||||
|
|
||||||
|
# An up-to-date Delta with all changes applied, representing
|
||||||
|
# the current state of the document
|
||||||
|
contents: [],
|
||||||
|
|
||||||
|
# The `inverted` versions of all changes performed on the
|
||||||
|
# document (useful for viewing history or undo the changes)
|
||||||
|
inverted_changes: []
|
||||||
|
}
|
||||||
|
|
||||||
|
# Public API
|
||||||
|
# ----------
|
||||||
|
|
||||||
|
def start_link(args), do: GenServer.start_link(__MODULE__, args, name: via_tuple(args.id))
|
||||||
|
def stop(pid), do: GenServer.stop(pid)
|
||||||
|
|
||||||
|
def update(id, %{"change" => change, "client_id" => client_id}),
|
||||||
|
do: GenServer.call(via_tuple(id), {:update, change, client_id})
|
||||||
|
|
||||||
|
def get_contents(id), do: GenServer.call(via_tuple(id), :get_contents)
|
||||||
|
def get_history(id), do: GenServer.call(via_tuple(id), :get_history)
|
||||||
|
def undo(id), do: GenServer.call(via_tuple(id), :undo)
|
||||||
|
|
||||||
|
# GenServer Callbacks
|
||||||
|
# -------------------
|
||||||
|
|
||||||
|
# Initialize the document with the default state
|
||||||
|
@impl true
|
||||||
|
def init(args) do
|
||||||
|
id = args.id
|
||||||
|
Registry.register(Poex.Pads.DocumentRegistry, id, [])
|
||||||
|
initial_state = Map.put(@initial_state, :id, id)
|
||||||
|
{:ok, initial_state}
|
||||||
|
end
|
||||||
|
|
||||||
|
# Apply a given change to the document, updating its contents
|
||||||
|
# and incrementing the version
|
||||||
|
#
|
||||||
|
# We also keep track of the inverted version of the change
|
||||||
|
# which is useful for performing undo or viewing history
|
||||||
|
@impl true
|
||||||
|
def handle_call({:update, change_map, client_id}, _from, state) do
|
||||||
|
change = DeltaUtils.convert_ops(change_map)
|
||||||
|
inverted = Delta.invert(change, state.contents)
|
||||||
|
|
||||||
|
state = %{
|
||||||
|
id: state.id,
|
||||||
|
version: state.version + 1,
|
||||||
|
contents: Delta.compose(state.contents, change),
|
||||||
|
inverted_changes: [inverted | state.inverted_changes]
|
||||||
|
}
|
||||||
|
|
||||||
|
PoexWeb.Endpoint.broadcast("pad:#{state.id}", "update", %{
|
||||||
|
change: change,
|
||||||
|
client_id: client_id
|
||||||
|
})
|
||||||
|
|
||||||
|
{:reply, state.contents, state}
|
||||||
|
end
|
||||||
|
|
||||||
|
# Fetch the current contents of the document
|
||||||
|
@impl true
|
||||||
|
def handle_call(:get_contents, _from, state) do
|
||||||
|
{:reply, state.contents, state}
|
||||||
|
end
|
||||||
|
|
||||||
|
# Revert the applied changes one by one to see how the
|
||||||
|
# document transformed over time
|
||||||
|
@impl true
|
||||||
|
def handle_call(:get_history, _from, state) do
|
||||||
|
current = {state.version, state.contents}
|
||||||
|
|
||||||
|
history =
|
||||||
|
Enum.scan(state.inverted_changes, current, fn inverted, {version, contents} ->
|
||||||
|
contents = Delta.compose(contents, inverted)
|
||||||
|
{version - 1, contents}
|
||||||
|
end)
|
||||||
|
|
||||||
|
{:reply, [current | history], state}
|
||||||
|
end
|
||||||
|
|
||||||
|
# Don't undo when document is already empty
|
||||||
|
@impl true
|
||||||
|
def handle_call(:undo, _from, %{version: 0} = state) do
|
||||||
|
{:reply, state.contents, state}
|
||||||
|
end
|
||||||
|
|
||||||
|
# Revert the last change, removing it from our stack and
|
||||||
|
# updating the contents
|
||||||
|
@impl true
|
||||||
|
def handle_call(:undo, _from, state) do
|
||||||
|
[last_change | changes] = state.inverted_changes
|
||||||
|
|
||||||
|
state = %{
|
||||||
|
id: state.id,
|
||||||
|
version: state.version - 1,
|
||||||
|
contents: Delta.compose(state.contents, last_change),
|
||||||
|
inverted_changes: changes
|
||||||
|
}
|
||||||
|
|
||||||
|
{:reply, state.contents, state}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp via_tuple(id), do: {:via, Registry, {Poex.Pads.DocumentRegistry, id}}
|
||||||
|
end
|
|
@ -0,0 +1,17 @@
|
||||||
|
defmodule Poex.Pads.DocumentDynamicSupervisor do
|
||||||
|
use DynamicSupervisor
|
||||||
|
alias Poex.Pads.DocumentServer
|
||||||
|
|
||||||
|
def start_link(init_arg) do
|
||||||
|
DynamicSupervisor.start_link(__MODULE__, init_arg, name: __MODULE__)
|
||||||
|
end
|
||||||
|
|
||||||
|
def start_document_supervisor(args) do
|
||||||
|
spec = {DocumentServer, args}
|
||||||
|
DynamicSupervisor.start_child(__MODULE__, spec)
|
||||||
|
end
|
||||||
|
|
||||||
|
def init(_init_arg) do
|
||||||
|
DynamicSupervisor.init(strategy: :one_for_one)
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,22 @@
|
||||||
|
defmodule Poex.Pads.Operation do
|
||||||
|
use Ecto.Schema
|
||||||
|
import Ecto.Changeset
|
||||||
|
|
||||||
|
@primary_key {:id, Ecto.UUID, autogenerate: true}
|
||||||
|
schema "operations" do
|
||||||
|
field :type, :string
|
||||||
|
field :value, :map
|
||||||
|
field :attributes, :map
|
||||||
|
belongs_to :document, Poex.Pads.Document
|
||||||
|
|
||||||
|
timestamps(type: :utc_datetime)
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc false
|
||||||
|
def changeset(operation, attrs) do
|
||||||
|
operation
|
||||||
|
|> cast(attrs, [:type, :value, :attributes])
|
||||||
|
|> validate_required([:type, :value])
|
||||||
|
|> assoc_constraint(:document)
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,28 @@
|
||||||
|
defmodule PoexWeb.PadChannel do
|
||||||
|
use Phoenix.Channel
|
||||||
|
alias Poex.Pads.DocumentServer
|
||||||
|
|
||||||
|
@moduledoc """
|
||||||
|
Channel for users of pad documents
|
||||||
|
"""
|
||||||
|
|
||||||
|
def join("pad:lobby", _message, socket) do
|
||||||
|
{:ok, socket}
|
||||||
|
end
|
||||||
|
|
||||||
|
def join("pad:" <> document_id, _params, socket) do
|
||||||
|
contents = DocumentServer.get_contents(document_id)
|
||||||
|
|
||||||
|
{:ok, %{uuid: socket.assigns.uuid, contents: contents},
|
||||||
|
assign(socket, :document_id, document_id)}
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_in("update", %{"change" => change, "client_id" => client_id}, socket) do
|
||||||
|
DocumentServer.update(socket.assigns.document_id, %{
|
||||||
|
"change" => change,
|
||||||
|
"client_id" => client_id
|
||||||
|
})
|
||||||
|
|
||||||
|
{:noreply, socket}
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,52 @@
|
||||||
|
defmodule PoexWeb.UserSocket do
|
||||||
|
alias Ecto.UUID
|
||||||
|
use Phoenix.Socket
|
||||||
|
|
||||||
|
# A Socket handler
|
||||||
|
#
|
||||||
|
# It's possible to control the websocket connection and
|
||||||
|
# assign values that can be accessed by your channel topics.
|
||||||
|
|
||||||
|
## Channels
|
||||||
|
channel "pad:*", PoexWeb.PadChannel
|
||||||
|
#
|
||||||
|
# To create a channel file, use the mix task:
|
||||||
|
#
|
||||||
|
# mix phx.gen.channel Room
|
||||||
|
#
|
||||||
|
# See the [`Channels guide`](https://hexdocs.pm/phoenix/channels.html)
|
||||||
|
# for further details.
|
||||||
|
|
||||||
|
# Socket params are passed from the client and can
|
||||||
|
# be used to verify and authenticate a user. After
|
||||||
|
# verification, you can put default assigns into
|
||||||
|
# the socket that will be set for all channels, ie
|
||||||
|
#
|
||||||
|
# {:ok, assign(socket, :user_id, verified_user_id)}
|
||||||
|
#
|
||||||
|
# To deny connection, return `:error` or `{:error, term}`. To control the
|
||||||
|
# response the client receives in that case, [define an error handler in the
|
||||||
|
# websocket
|
||||||
|
# configuration](https://hexdocs.pm/phoenix/Phoenix.Endpoint.html#socket/3-websocket-configuration).
|
||||||
|
#
|
||||||
|
# See `Phoenix.Token` documentation for examples in
|
||||||
|
# performing token verification on connect.
|
||||||
|
@impl true
|
||||||
|
def connect(_params, socket, _connect_info) do
|
||||||
|
uuid = UUID.generate()
|
||||||
|
{:ok, assign(socket, :uuid, uuid)}
|
||||||
|
end
|
||||||
|
|
||||||
|
# Socket id's are topics that allow you to identify all sockets for a given user:
|
||||||
|
#
|
||||||
|
# def id(socket), do: "user_socket:#{socket.assigns.user_id}"
|
||||||
|
#
|
||||||
|
# Would allow you to broadcast a "disconnect" event and terminate
|
||||||
|
# all active sockets and channels for a given user:
|
||||||
|
#
|
||||||
|
# Elixir.PoexWeb.Endpoint.broadcast("user_socket:#{user.id}", "disconnect", %{})
|
||||||
|
#
|
||||||
|
# Returning `nil` makes this socket anonymous.
|
||||||
|
@impl true
|
||||||
|
def id(_socket), do: nil
|
||||||
|
end
|
|
@ -1,4 +1,5 @@
|
||||||
defmodule PoexWeb.Layouts do
|
defmodule PoexWeb.Layouts do
|
||||||
|
@moduledoc false
|
||||||
use PoexWeb, :html
|
use PoexWeb, :html
|
||||||
|
|
||||||
embed_templates "layouts/*"
|
embed_templates "layouts/*"
|
||||||
|
|
|
@ -1,32 +1,4 @@
|
||||||
<header class="px-4 sm:px-6 lg:px-8">
|
<main>
|
||||||
<div class="flex items-center justify-between border-b border-zinc-100 py-3 text-sm">
|
|
||||||
<div class="flex items-center gap-4">
|
|
||||||
<a href="/">
|
|
||||||
<img src={~p"/images/logo.svg"} width="36" />
|
|
||||||
</a>
|
|
||||||
<p class="bg-brand/5 text-brand rounded-full px-2 font-medium leading-6">
|
|
||||||
v<%= Application.spec(:phoenix, :vsn) %>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-4 font-semibold leading-6 text-zinc-900">
|
|
||||||
<a href="https://twitter.com/elixirphoenix" class="hover:text-zinc-700">
|
|
||||||
@elixirphoenix
|
|
||||||
</a>
|
|
||||||
<a href="https://github.com/phoenixframework/phoenix" class="hover:text-zinc-700">
|
|
||||||
GitHub
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
href="https://hexdocs.pm/phoenix/overview.html"
|
|
||||||
class="rounded-lg bg-zinc-100 px-2 py-1 hover:bg-zinc-200/80"
|
|
||||||
>
|
|
||||||
Get Started <span aria-hidden="true">→</span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
<main class="px-4 py-20 sm:px-6 lg:px-8">
|
|
||||||
<div class="mx-auto max-w-2xl">
|
|
||||||
<.flash_group flash={@flash} />
|
<.flash_group flash={@flash} />
|
||||||
<%= @inner_content %>
|
<%= @inner_content %>
|
||||||
</div>
|
|
||||||
</main>
|
</main>
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
<%= assigns[:page_title] || "Poex" %>
|
<%= assigns[:page_title] || "Poex" %>
|
||||||
</.live_title>
|
</.live_title>
|
||||||
<link phx-track-static rel="stylesheet" href={~p"/assets/app.css"} />
|
<link phx-track-static rel="stylesheet" href={~p"/assets/app.css"} />
|
||||||
|
<link href="https://cdn.quilljs.com/1.3.7/quill.snow.css" rel="stylesheet">
|
||||||
<script defer phx-track-static type="text/javascript" src={~p"/assets/app.js"}>
|
<script defer phx-track-static type="text/javascript" src={~p"/assets/app.js"}>
|
||||||
</script>
|
</script>
|
||||||
</head>
|
</head>
|
||||||
|
|
|
@ -1,222 +1,22 @@
|
||||||
<.flash_group flash={@flash} />
|
<.flash_group flash={@flash} />
|
||||||
<div class="left-[40rem] fixed inset-y-0 right-0 z-0 hidden lg:block xl:left-[50rem]">
|
|
||||||
<svg
|
<div class="mx-auto max-w-2xl">
|
||||||
viewBox="0 0 1480 957"
|
<div class="flex items-center justify-center bg-gray-100">
|
||||||
fill="none"
|
<div class="text-white text-center">
|
||||||
aria-hidden="true"
|
<h1 class="text-4xl font-semibold text-zinc-900">
|
||||||
class="absolute inset-0 h-full w-full"
|
Poex
|
||||||
preserveAspectRatio="xMinYMid slice"
|
|
||||||
>
|
|
||||||
<path fill="#EE7868" d="M0 0h1480v957H0z" />
|
|
||||||
<path
|
|
||||||
d="M137.542 466.27c-582.851-48.41-988.806-82.127-1608.412 658.2l67.39 810 3083.15-256.51L1535.94-49.622l-98.36 8.183C1269.29 281.468 734.115 515.799 146.47 467.012l-8.928-.742Z"
|
|
||||||
fill="#FF9F92"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M371.028 528.664C-169.369 304.988-545.754 149.198-1361.45 665.565l-182.58 792.025 3014.73 694.98 389.42-1689.25-96.18-22.171C1505.28 697.438 924.153 757.586 379.305 532.09l-8.277-3.426Z"
|
|
||||||
fill="#FA8372"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M359.326 571.714C-104.765 215.795-428.003-32.102-1349.55 255.554l-282.3 1224.596 3047.04 722.01 312.24-1354.467C1411.25 1028.3 834.355 935.995 366.435 577.166l-7.109-5.452Z"
|
|
||||||
fill="#E96856"
|
|
||||||
fill-opacity=".6"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M1593.87 1236.88c-352.15 92.63-885.498-145.85-1244.602-613.557l-5.455-7.105C-12.347 152.31-260.41-170.8-1225-131.458l-368.63 1599.048 3057.19 704.76 130.31-935.47Z"
|
|
||||||
fill="#C42652"
|
|
||||||
fill-opacity=".2"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M1411.91 1526.93c-363.79 15.71-834.312-330.6-1085.883-863.909l-3.822-8.102C72.704 125.95-101.074-242.476-1052.01-408.907l-699.85 1484.267 2837.75 1338.01 326.02-886.44Z"
|
|
||||||
fill="#A41C42"
|
|
||||||
fill-opacity=".2"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M1116.26 1863.69c-355.457-78.98-720.318-535.27-825.287-1115.521l-1.594-8.816C185.286 163.833 112.786-237.016-762.678-643.898L-1822.83 608.665 571.922 2635.55l544.338-771.86Z"
|
|
||||||
fill="#A41C42"
|
|
||||||
fill-opacity=".2"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div class="px-4 py-10 sm:px-6 sm:py-28 lg:px-8 xl:px-28 xl:py-32">
|
|
||||||
<div class="mx-auto max-w-xl lg:mx-0">
|
|
||||||
<svg viewBox="0 0 71 48" class="h-12" aria-hidden="true">
|
|
||||||
<path
|
|
||||||
d="m26.371 33.477-.552-.1c-3.92-.729-6.397-3.1-7.57-6.829-.733-2.324.597-4.035 3.035-4.148 1.995-.092 3.362 1.055 4.57 2.39 1.557 1.72 2.984 3.558 4.514 5.305 2.202 2.515 4.797 4.134 8.347 3.634 3.183-.448 5.958-1.725 8.371-3.828.363-.316.761-.592 1.144-.886l-.241-.284c-2.027.63-4.093.841-6.205.735-3.195-.16-6.24-.828-8.964-2.582-2.486-1.601-4.319-3.746-5.19-6.611-.704-2.315.736-3.934 3.135-3.6.948.133 1.746.56 2.463 1.165.583.493 1.143 1.015 1.738 1.493 2.8 2.25 6.712 2.375 10.265-.068-5.842-.026-9.817-3.24-13.308-7.313-1.366-1.594-2.7-3.216-4.095-4.785-2.698-3.036-5.692-5.71-9.79-6.623C12.8-.623 7.745.14 2.893 2.361 1.926 2.804.997 3.319 0 4.149c.494 0 .763.006 1.032 0 2.446-.064 4.28 1.023 5.602 3.024.962 1.457 1.415 3.104 1.761 4.798.513 2.515.247 5.078.544 7.605.761 6.494 4.08 11.026 10.26 13.346 2.267.852 4.591 1.135 7.172.555ZM10.751 3.852c-.976.246-1.756-.148-2.56-.962 1.377-.343 2.592-.476 3.897-.528-.107.848-.607 1.306-1.336 1.49Zm32.002 37.924c-.085-.626-.62-.901-1.04-1.228-1.857-1.446-4.03-1.958-6.333-2-1.375-.026-2.735-.128-4.031-.61-.595-.22-1.26-.505-1.244-1.272.015-.78.693-1 1.31-1.184.505-.15 1.026-.247 1.6-.382-1.46-.936-2.886-1.065-4.787-.3-2.993 1.202-5.943 1.06-8.926-.017-1.684-.608-3.179-1.563-4.735-2.408l-.043.03a2.96 2.96 0 0 0 .04-.029c-.038-.117-.107-.12-.197-.054l.122.107c1.29 2.115 3.034 3.817 5.004 5.271 3.793 2.8 7.936 4.471 12.784 3.73A66.714 66.714 0 0 1 37 40.877c1.98-.16 3.866.398 5.753.899Zm-9.14-30.345c-.105-.076-.206-.266-.42-.069 1.745 2.36 3.985 4.098 6.683 5.193 4.354 1.767 8.773 2.07 13.293.51 3.51-1.21 6.033-.028 7.343 3.38.19-3.955-2.137-6.837-5.843-7.401-2.084-.318-4.01.373-5.962.94-5.434 1.575-10.485.798-15.094-2.553Zm27.085 15.425c.708.059 1.416.123 2.124.185-1.6-1.405-3.55-1.517-5.523-1.404-3.003.17-5.167 1.903-7.14 3.972-1.739 1.824-3.31 3.87-5.903 4.604.043.078.054.117.066.117.35.005.699.021 1.047.005 3.768-.17 7.317-.965 10.14-3.7.89-.86 1.685-1.817 2.544-2.71.716-.746 1.584-1.159 2.645-1.07Zm-8.753-4.67c-2.812.246-5.254 1.409-7.548 2.943-1.766 1.18-3.654 1.738-5.776 1.37-.374-.066-.75-.114-1.124-.17l-.013.156c.135.07.265.151.405.207.354.14.702.308 1.07.395 4.083.971 7.992.474 11.516-1.803 2.221-1.435 4.521-1.707 7.013-1.336.252.038.503.083.756.107.234.022.479.255.795.003-2.179-1.574-4.526-2.096-7.094-1.872Zm-10.049-9.544c1.475.051 2.943-.142 4.486-1.059-.452.04-.643.04-.827.076-2.126.424-4.033-.04-5.733-1.383-.623-.493-1.257-.974-1.889-1.457-2.503-1.914-5.374-2.555-8.514-2.5.05.154.054.26.108.315 3.417 3.455 7.371 5.836 12.369 6.008Zm24.727 17.731c-2.114-2.097-4.952-2.367-7.578-.537 1.738.078 3.043.632 4.101 1.728.374.388.763.768 1.182 1.106 1.6 1.29 4.311 1.352 5.896.155-1.861-.726-1.861-.726-3.601-2.452Zm-21.058 16.06c-1.858-3.46-4.981-4.24-8.59-4.008a9.667 9.667 0 0 1 2.977 1.39c.84.586 1.547 1.311 2.243 2.055 1.38 1.473 3.534 2.376 4.962 2.07-.656-.412-1.238-.848-1.592-1.507Zm17.29-19.32c0-.023.001-.045.003-.068l-.006.006.006-.006-.036-.004.021.018.012.053Zm-20 14.744a7.61 7.61 0 0 0-.072-.041.127.127 0 0 0 .015.043c.005.008.038 0 .058-.002Zm-.072-.041-.008-.034-.008.01.008-.01-.022-.006.005.026.024.014Z"
|
|
||||||
fill="#FD4F00"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
<h1 class="text-brand mt-10 flex items-center text-sm font-semibold leading-6">
|
|
||||||
Phoenix Framework
|
|
||||||
<small class="bg-brand/5 text-[0.8125rem] ml-3 rounded-full px-2 font-medium leading-6">
|
|
||||||
v<%= Application.spec(:phoenix, :vsn) %>
|
|
||||||
</small>
|
|
||||||
</h1>
|
</h1>
|
||||||
<p class="text-[2rem] mt-4 font-semibold leading-10 tracking-tighter text-zinc-900">
|
<p class="mt-4 text-lg text-zinc-600">
|
||||||
Peace of mind from prototype to production.
|
The pad for poets
|
||||||
</p>
|
</p>
|
||||||
<p class="mt-4 text-base leading-7 text-zinc-600">
|
<div class="mt-10">
|
||||||
Build rich, interactive web applications quickly, with less code and fewer moving parts. Join our growing community of developers using Phoenix to craft APIs, HTML5 apps and more, for fun or at scale.
|
<.link
|
||||||
</p>
|
href={~p"/pad/new"}
|
||||||
<div class="flex">
|
class="inline-block bg-zinc-900 text-white font-semibold px-6 py-3 rounded-lg hover:bg-zinc-700"
|
||||||
<div class="w-full sm:w-auto">
|
|
||||||
<div class="mt-10 grid grid-cols-1 gap-x-6 gap-y-4 sm:grid-cols-3">
|
|
||||||
<a
|
|
||||||
href="https://hexdocs.pm/phoenix/overview.html"
|
|
||||||
class="group relative rounded-2xl px-6 py-4 text-sm font-semibold leading-6 text-zinc-900 sm:py-6"
|
|
||||||
>
|
>
|
||||||
<span class="absolute inset-0 rounded-2xl bg-zinc-50 transition group-hover:bg-zinc-100 sm:group-hover:scale-105">
|
Begin
|
||||||
</span>
|
</.link>
|
||||||
<span class="relative flex items-center gap-4 sm:flex-col">
|
|
||||||
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true" class="h-6 w-6">
|
|
||||||
<path d="m12 4 10-2v18l-10 2V4Z" fill="#18181B" fill-opacity=".15" />
|
|
||||||
<path
|
|
||||||
d="M12 4 2 2v18l10 2m0-18v18m0-18 10-2v18l-10 2"
|
|
||||||
stroke="#18181B"
|
|
||||||
stroke-width="2"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
Guides & Docs
|
|
||||||
</span>
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
href="https://github.com/phoenixframework/phoenix"
|
|
||||||
class="group relative rounded-2xl px-6 py-4 text-sm font-semibold leading-6 text-zinc-900 sm:py-6"
|
|
||||||
>
|
|
||||||
<span class="absolute inset-0 rounded-2xl bg-zinc-50 transition group-hover:bg-zinc-100 sm:group-hover:scale-105">
|
|
||||||
</span>
|
|
||||||
<span class="relative flex items-center gap-4 sm:flex-col">
|
|
||||||
<svg viewBox="0 0 24 24" aria-hidden="true" class="h-6 w-6">
|
|
||||||
<path
|
|
||||||
fill-rule="evenodd"
|
|
||||||
clip-rule="evenodd"
|
|
||||||
d="M12 0C5.37 0 0 5.506 0 12.303c0 5.445 3.435 10.043 8.205 11.674.6.107.825-.262.825-.585 0-.292-.015-1.261-.015-2.291C6 21.67 5.22 20.346 4.98 19.654c-.135-.354-.72-1.446-1.23-1.738-.42-.23-1.02-.8-.015-.815.945-.015 1.62.892 1.845 1.261 1.08 1.86 2.805 1.338 3.495 1.015.105-.8.42-1.338.765-1.645-2.67-.308-5.46-1.37-5.46-6.075 0-1.338.465-2.446 1.23-3.307-.12-.308-.54-1.569.12-3.26 0 0 1.005-.323 3.3 1.26.96-.276 1.98-.415 3-.415s2.04.139 3 .416c2.295-1.6 3.3-1.261 3.3-1.261.66 1.691.24 2.952.12 3.26.765.861 1.23 1.953 1.23 3.307 0 4.721-2.805 5.767-5.475 6.075.435.384.81 1.122.81 2.276 0 1.645-.015 2.968-.015 3.383 0 .323.225.707.825.585a12.047 12.047 0 0 0 5.919-4.489A12.536 12.536 0 0 0 24 12.304C24 5.505 18.63 0 12 0Z"
|
|
||||||
fill="#18181B"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
Source Code
|
|
||||||
</span>
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
href={"https://github.com/phoenixframework/phoenix/blob/v#{Application.spec(:phoenix, :vsn)}/CHANGELOG.md"}
|
|
||||||
class="group relative rounded-2xl px-6 py-4 text-sm font-semibold leading-6 text-zinc-900 sm:py-6"
|
|
||||||
>
|
|
||||||
<span class="absolute inset-0 rounded-2xl bg-zinc-50 transition group-hover:bg-zinc-100 sm:group-hover:scale-105">
|
|
||||||
</span>
|
|
||||||
<span class="relative flex items-center gap-4 sm:flex-col">
|
|
||||||
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true" class="h-6 w-6">
|
|
||||||
<path
|
|
||||||
d="M12 1v6M12 17v6"
|
|
||||||
stroke="#18181B"
|
|
||||||
stroke-width="2"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
/>
|
|
||||||
<circle
|
|
||||||
cx="12"
|
|
||||||
cy="12"
|
|
||||||
r="4"
|
|
||||||
fill="#18181B"
|
|
||||||
fill-opacity=".15"
|
|
||||||
stroke="#18181B"
|
|
||||||
stroke-width="2"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
Changelog
|
|
||||||
</span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div class="mt-10 grid grid-cols-1 gap-y-4 text-sm leading-6 text-zinc-700 sm:grid-cols-2">
|
|
||||||
<div>
|
|
||||||
<a
|
|
||||||
href="https://twitter.com/elixirphoenix"
|
|
||||||
class="group -mx-2 -my-0.5 inline-flex items-center gap-3 rounded-lg px-2 py-0.5 hover:bg-zinc-50 hover:text-zinc-900"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
viewBox="0 0 16 16"
|
|
||||||
aria-hidden="true"
|
|
||||||
class="h-4 w-4 fill-zinc-400 group-hover:fill-zinc-600"
|
|
||||||
>
|
|
||||||
<path d="M5.403 14c5.283 0 8.172-4.617 8.172-8.62 0-.131 0-.262-.008-.391A6.033 6.033 0 0 0 15 3.419a5.503 5.503 0 0 1-1.65.477 3.018 3.018 0 0 0 1.263-1.676 5.579 5.579 0 0 1-1.824.736 2.832 2.832 0 0 0-1.63-.916 2.746 2.746 0 0 0-1.821.319A2.973 2.973 0 0 0 8.076 3.78a3.185 3.185 0 0 0-.182 1.938 7.826 7.826 0 0 1-3.279-.918 8.253 8.253 0 0 1-2.64-2.247 3.176 3.176 0 0 0-.315 2.208 3.037 3.037 0 0 0 1.203 1.836A2.739 2.739 0 0 1 1.56 6.22v.038c0 .7.23 1.377.65 1.919.42.54 1.004.912 1.654 1.05-.423.122-.866.14-1.297.052.184.602.541 1.129 1.022 1.506a2.78 2.78 0 0 0 1.662.598 5.656 5.656 0 0 1-2.007 1.074A5.475 5.475 0 0 1 1 12.64a7.827 7.827 0 0 0 4.403 1.358" />
|
|
||||||
</svg>
|
|
||||||
Follow on Twitter
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<a
|
|
||||||
href="https://elixirforum.com"
|
|
||||||
class="group -mx-2 -my-0.5 inline-flex items-center gap-3 rounded-lg px-2 py-0.5 hover:bg-zinc-50 hover:text-zinc-900"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
viewBox="0 0 16 16"
|
|
||||||
aria-hidden="true"
|
|
||||||
class="h-4 w-4 fill-zinc-400 group-hover:fill-zinc-600"
|
|
||||||
>
|
|
||||||
<path d="M8 13.833c3.866 0 7-2.873 7-6.416C15 3.873 11.866 1 8 1S1 3.873 1 7.417c0 1.081.292 2.1.808 2.995.606 1.05.806 2.399.086 3.375l-.208.283c-.285.386-.01.905.465.85.852-.098 2.048-.318 3.137-.81a3.717 3.717 0 0 1 1.91-.318c.263.027.53.041.802.041Z" />
|
|
||||||
</svg>
|
|
||||||
Discuss on the Elixir Forum
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<a
|
|
||||||
href="https://web.libera.chat/#elixir"
|
|
||||||
class="group -mx-2 -my-0.5 inline-flex items-center gap-3 rounded-lg px-2 py-0.5 hover:bg-zinc-50 hover:text-zinc-900"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
viewBox="0 0 16 16"
|
|
||||||
aria-hidden="true"
|
|
||||||
class="h-4 w-4 fill-zinc-400 group-hover:fill-zinc-600"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fill-rule="evenodd"
|
|
||||||
clip-rule="evenodd"
|
|
||||||
d="M6.356 2.007a.75.75 0 0 1 .637.849l-1.5 10.5a.75.75 0 1 1-1.485-.212l1.5-10.5a.75.75 0 0 1 .848-.637ZM11.356 2.008a.75.75 0 0 1 .637.848l-1.5 10.5a.75.75 0 0 1-1.485-.212l1.5-10.5a.75.75 0 0 1 .848-.636Z"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
fill-rule="evenodd"
|
|
||||||
clip-rule="evenodd"
|
|
||||||
d="M14 5.25a.75.75 0 0 1-.75.75h-9.5a.75.75 0 0 1 0-1.5h9.5a.75.75 0 0 1 .75.75ZM13 10.75a.75.75 0 0 1-.75.75h-9.5a.75.75 0 0 1 0-1.5h9.5a.75.75 0 0 1 .75.75Z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
Chat on Libera IRC
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<a
|
|
||||||
href="https://discord.gg/elixir"
|
|
||||||
class="group -mx-2 -my-0.5 inline-flex items-center gap-3 rounded-lg px-2 py-0.5 hover:bg-zinc-50 hover:text-zinc-900"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
viewBox="0 0 16 16"
|
|
||||||
aria-hidden="true"
|
|
||||||
class="h-4 w-4 fill-zinc-400 group-hover:fill-zinc-600"
|
|
||||||
>
|
|
||||||
<path d="M13.545 2.995c-1.02-.46-2.114-.8-3.257-.994a.05.05 0 0 0-.052.024c-.141.246-.297.567-.406.82a12.377 12.377 0 0 0-3.658 0 8.238 8.238 0 0 0-.412-.82.052.052 0 0 0-.052-.024 13.315 13.315 0 0 0-3.257.994.046.046 0 0 0-.021.018C.356 6.063-.213 9.036.066 11.973c.001.015.01.029.02.038a13.353 13.353 0 0 0 3.996 1.987.052.052 0 0 0 .056-.018c.308-.414.582-.85.818-1.309a.05.05 0 0 0-.028-.069 8.808 8.808 0 0 1-1.248-.585.05.05 0 0 1-.005-.084c.084-.062.168-.126.248-.191a.05.05 0 0 1 .051-.007c2.619 1.176 5.454 1.176 8.041 0a.05.05 0 0 1 .053.006c.08.065.164.13.248.192a.05.05 0 0 1-.004.084c-.399.23-.813.423-1.249.585a.05.05 0 0 0-.027.07c.24.457.514.893.817 1.307a.051.051 0 0 0 .056.019 13.31 13.31 0 0 0 4.001-1.987.05.05 0 0 0 .021-.037c.334-3.396-.559-6.345-2.365-8.96a.04.04 0 0 0-.021-.02Zm-8.198 7.19c-.789 0-1.438-.712-1.438-1.587 0-.874.637-1.586 1.438-1.586.807 0 1.45.718 1.438 1.586 0 .875-.637 1.587-1.438 1.587Zm5.316 0c-.788 0-1.438-.712-1.438-1.587 0-.874.637-1.586 1.438-1.586.807 0 1.45.718 1.438 1.586 0 .875-.63 1.587-1.438 1.587Z" />
|
|
||||||
</svg>
|
|
||||||
Join our Discord server
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<a
|
|
||||||
href="https://fly.io/docs/elixir/getting-started/"
|
|
||||||
class="group -mx-2 -my-0.5 inline-flex items-center gap-3 rounded-lg px-2 py-0.5 hover:bg-zinc-50 hover:text-zinc-900"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
viewBox="0 0 20 20"
|
|
||||||
aria-hidden="true"
|
|
||||||
class="h-4 w-4 fill-zinc-400 group-hover:fill-zinc-600"
|
|
||||||
>
|
|
||||||
<path d="M1 12.5A4.5 4.5 0 005.5 17H15a4 4 0 001.866-7.539 3.504 3.504 0 00-4.504-4.272A4.5 4.5 0 004.06 8.235 4.502 4.502 0 001 12.5z" />
|
|
||||||
</svg>
|
|
||||||
Deploy your application
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
|
@ -13,6 +13,10 @@ defmodule PoexWeb.Endpoint do
|
||||||
|
|
||||||
socket "/live", Phoenix.LiveView.Socket, websocket: [connect_info: [session: @session_options]]
|
socket "/live", Phoenix.LiveView.Socket, websocket: [connect_info: [session: @session_options]]
|
||||||
|
|
||||||
|
socket "/socket", PoexWeb.UserSocket,
|
||||||
|
websocket: true,
|
||||||
|
longpoll: false
|
||||||
|
|
||||||
# Serve at "/" the static files from "priv/static" directory.
|
# Serve at "/" the static files from "priv/static" directory.
|
||||||
#
|
#
|
||||||
# You should set gzip to true if you are running phx.digest
|
# You should set gzip to true if you are running phx.digest
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
defmodule PoexWeb.PadLive do
|
||||||
|
alias Poex.Pads
|
||||||
|
alias Poex.Pads.Document
|
||||||
|
alias Poex.Repo
|
||||||
|
alias Poex.Utils
|
||||||
|
|
||||||
|
use PoexWeb, :live_view
|
||||||
|
|
||||||
|
def mount(%{"id" => "new"}, _session, socket) do
|
||||||
|
# Create a new PadDocument
|
||||||
|
{:ok, new_document} =
|
||||||
|
Document.changeset(%Document{}, %{title: "Untitled"})
|
||||||
|
|> Repo.insert()
|
||||||
|
|
||||||
|
Poex.Pads.DocumentDynamicSupervisor.start_document_supervisor(new_document)
|
||||||
|
|
||||||
|
# Redirect to the new document with its ID
|
||||||
|
{:ok, push_navigate(socket, to: ~p"/pad/#{new_document.id}", replace: true)}
|
||||||
|
end
|
||||||
|
|
||||||
|
def mount(%{"id" => id}, _session, socket) do
|
||||||
|
%{
|
||||||
|
id: id,
|
||||||
|
title: title,
|
||||||
|
state: state
|
||||||
|
} = Repo.get!(Document, id)
|
||||||
|
|
||||||
|
# init editor and assigns with latest state from doc
|
||||||
|
{:ok, assign(socket, id: id, title: title, state: state |> Utils.atomize_keys())}
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_info(:new_document, socket) do
|
||||||
|
{:noreply, push_navigate(socket, to: "/pad/new")}
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,5 @@
|
||||||
|
<main class="container h-96 mx-auto py-12">
|
||||||
|
<div id="poex-text-editor" phx-hook="TextEditor" data-pad-id={@id} class="flex-auto">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</main>
|
|
@ -61,6 +61,16 @@ defmodule PoexWeb.Router do
|
||||||
post "/users/log_in", UserSessionController, :create
|
post "/users/log_in", UserSessionController, :create
|
||||||
end
|
end
|
||||||
|
|
||||||
|
scope "/", PoexWeb do
|
||||||
|
pipe_through [:browser]
|
||||||
|
|
||||||
|
live_session :pad,
|
||||||
|
on_mount: [{PoexWeb.UserAuth, :mount_current_user}] do
|
||||||
|
live "/pad/:id", PadLive
|
||||||
|
live "/pad/new", PadLive, :new_document
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
scope "/", PoexWeb do
|
scope "/", PoexWeb do
|
||||||
pipe_through [:browser, :require_authenticated_user]
|
pipe_through [:browser, :require_authenticated_user]
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
defmodule PoexWeb.Telemetry do
|
defmodule PoexWeb.Telemetry do
|
||||||
|
@moduledoc false
|
||||||
use Supervisor
|
use Supervisor
|
||||||
import Telemetry.Metrics
|
import Telemetry.Metrics
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,8 @@
|
||||||
defmodule PoexWeb.UserAuth do
|
defmodule PoexWeb.UserAuth do
|
||||||
|
@moduledoc """
|
||||||
|
This module handles user sessions
|
||||||
|
"""
|
||||||
|
|
||||||
use PoexWeb, :verified_routes
|
use PoexWeb, :verified_routes
|
||||||
|
|
||||||
import Plug.Conn
|
import Plug.Conn
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
defmodule Poex.Utils.DeltaUtils do
|
||||||
|
alias Delta.Op
|
||||||
|
|
||||||
|
def convert_ops(map) do
|
||||||
|
ops = Map.get(map, "ops") || []
|
||||||
|
|
||||||
|
Enum.map(ops, fn op ->
|
||||||
|
type = Map.keys(op) |> List.first()
|
||||||
|
value = Map.get(op, type)
|
||||||
|
attrs = Map.get(op, "attributes")
|
||||||
|
|
||||||
|
case type do
|
||||||
|
"insert" -> Op.insert(value, attrs)
|
||||||
|
"delete" -> Op.delete(value)
|
||||||
|
"retain" -> Op.retain(value, attrs)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,18 @@
|
||||||
|
defmodule Poex.Utils do
|
||||||
|
def atomize_keys(%{__struct__: _} = map), do: map
|
||||||
|
|
||||||
|
def atomize_keys(map) when is_map(map),
|
||||||
|
do:
|
||||||
|
Map.new(map, fn {k, v} ->
|
||||||
|
{
|
||||||
|
atomize_key(k),
|
||||||
|
atomize_keys(v)
|
||||||
|
}
|
||||||
|
end)
|
||||||
|
|
||||||
|
def atomize_keys(list) when is_list(list), do: Enum.map(list, &atomize_keys(&1))
|
||||||
|
def atomize_keys(map), do: map
|
||||||
|
|
||||||
|
defp atomize_key(key) when is_bitstring(key), do: String.to_atom(key)
|
||||||
|
defp atomize_key(key), do: key
|
||||||
|
end
|
4
mix.exs
4
mix.exs
|
@ -51,7 +51,9 @@ defmodule Poex.MixProject do
|
||||||
{:gettext, "~> 0.20"},
|
{:gettext, "~> 0.20"},
|
||||||
{:jason, "~> 1.2"},
|
{:jason, "~> 1.2"},
|
||||||
{:dns_cluster, "~> 0.1.1"},
|
{:dns_cluster, "~> 0.1.1"},
|
||||||
{:plug_cowboy, "~> 2.5"}
|
{:plug_cowboy, "~> 2.5"},
|
||||||
|
{:delta, "~> 0.2.0"},
|
||||||
|
{:credo, "~> 1.7", only: [:dev, :test], runtime: false}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
4
mix.lock
4
mix.lock
|
@ -1,12 +1,15 @@
|
||||||
%{
|
%{
|
||||||
"bcrypt_elixir": {:hex, :bcrypt_elixir, "3.1.0", "0b110a9a6c619b19a7f73fa3004aa11d6e719a67e672d1633dc36b6b2290a0f7", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "2ad2acb5a8bc049e8d5aa267802631912bb80d5f4110a178ae7999e69dca1bf7"},
|
"bcrypt_elixir": {:hex, :bcrypt_elixir, "3.1.0", "0b110a9a6c619b19a7f73fa3004aa11d6e719a67e672d1633dc36b6b2290a0f7", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "2ad2acb5a8bc049e8d5aa267802631912bb80d5f4110a178ae7999e69dca1bf7"},
|
||||||
|
"bunt": {:hex, :bunt, "0.2.1", "e2d4792f7bc0ced7583ab54922808919518d0e57ee162901a16a1b6664ef3b14", [:mix], [], "hexpm", "a330bfb4245239787b15005e66ae6845c9cd524a288f0d141c148b02603777a5"},
|
||||||
"castore": {:hex, :castore, "1.0.4", "ff4d0fb2e6411c0479b1d965a814ea6d00e51eb2f58697446e9c41a97d940b28", [:mix], [], "hexpm", "9418c1b8144e11656f0be99943db4caf04612e3eaecefb5dae9a2a87565584f8"},
|
"castore": {:hex, :castore, "1.0.4", "ff4d0fb2e6411c0479b1d965a814ea6d00e51eb2f58697446e9c41a97d940b28", [:mix], [], "hexpm", "9418c1b8144e11656f0be99943db4caf04612e3eaecefb5dae9a2a87565584f8"},
|
||||||
"comeonin": {:hex, :comeonin, "5.4.0", "246a56ca3f41d404380fc6465650ddaa532c7f98be4bda1b4656b3a37cc13abe", [:mix], [], "hexpm", "796393a9e50d01999d56b7b8420ab0481a7538d0caf80919da493b4a6e51faf1"},
|
"comeonin": {:hex, :comeonin, "5.4.0", "246a56ca3f41d404380fc6465650ddaa532c7f98be4bda1b4656b3a37cc13abe", [:mix], [], "hexpm", "796393a9e50d01999d56b7b8420ab0481a7538d0caf80919da493b4a6e51faf1"},
|
||||||
"cowboy": {:hex, :cowboy, "2.10.0", "ff9ffeff91dae4ae270dd975642997afe2a1179d94b1887863e43f681a203e26", [:make, :rebar3], [{:cowlib, "2.12.1", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "3afdccb7183cc6f143cb14d3cf51fa00e53db9ec80cdcd525482f5e99bc41d6b"},
|
"cowboy": {:hex, :cowboy, "2.10.0", "ff9ffeff91dae4ae270dd975642997afe2a1179d94b1887863e43f681a203e26", [:make, :rebar3], [{:cowlib, "2.12.1", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "3afdccb7183cc6f143cb14d3cf51fa00e53db9ec80cdcd525482f5e99bc41d6b"},
|
||||||
"cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"},
|
"cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"},
|
||||||
"cowlib": {:hex, :cowlib, "2.12.1", "a9fa9a625f1d2025fe6b462cb865881329b5caff8f1854d1cbc9f9533f00e1e1", [:make, :rebar3], [], "hexpm", "163b73f6367a7341b33c794c4e88e7dbfe6498ac42dcd69ef44c5bc5507c8db0"},
|
"cowlib": {:hex, :cowlib, "2.12.1", "a9fa9a625f1d2025fe6b462cb865881329b5caff8f1854d1cbc9f9533f00e1e1", [:make, :rebar3], [], "hexpm", "163b73f6367a7341b33c794c4e88e7dbfe6498ac42dcd69ef44c5bc5507c8db0"},
|
||||||
|
"credo": {:hex, :credo, "1.7.1", "6e26bbcc9e22eefbff7e43188e69924e78818e2fe6282487d0703652bc20fd62", [:mix], [{:bunt, "~> 0.2.1", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "e9871c6095a4c0381c89b6aa98bc6260a8ba6addccf7f6a53da8849c748a58a2"},
|
||||||
"db_connection": {:hex, :db_connection, "2.6.0", "77d835c472b5b67fc4f29556dee74bf511bbafecdcaf98c27d27fa5918152086", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c2f992d15725e721ec7fbc1189d4ecdb8afef76648c746a8e1cad35e3b8a35f3"},
|
"db_connection": {:hex, :db_connection, "2.6.0", "77d835c472b5b67fc4f29556dee74bf511bbafecdcaf98c27d27fa5918152086", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c2f992d15725e721ec7fbc1189d4ecdb8afef76648c746a8e1cad35e3b8a35f3"},
|
||||||
"decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"},
|
"decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"},
|
||||||
|
"delta": {:hex, :delta, "0.2.0", "1e0454b103a70e3c8465666588eeb14fa099ae40b5c5eefab035a05609cd345f", [:mix], [], "hexpm", "5b091fb47c8a9307ff39c2fd807493369bd92e00dbc8a65bc4d3135170cadcc5"},
|
||||||
"dns_cluster": {:hex, :dns_cluster, "0.1.1", "73b4b2c3ec692f8a64276c43f8c929733a9ab9ac48c34e4c0b3d9d1b5cd69155", [:mix], [], "hexpm", "03a3f6ff16dcbb53e219b99c7af6aab29eb6b88acf80164b4bd76ac18dc890b3"},
|
"dns_cluster": {:hex, :dns_cluster, "0.1.1", "73b4b2c3ec692f8a64276c43f8c929733a9ab9ac48c34e4c0b3d9d1b5cd69155", [:mix], [], "hexpm", "03a3f6ff16dcbb53e219b99c7af6aab29eb6b88acf80164b4bd76ac18dc890b3"},
|
||||||
"ecto": {:hex, :ecto, "3.10.3", "eb2ae2eecd210b4eb8bece1217b297ad4ff824b4384c0e3fdd28aaf96edd6135", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "44bec74e2364d491d70f7e42cd0d690922659d329f6465e89feb8a34e8cd3433"},
|
"ecto": {:hex, :ecto, "3.10.3", "eb2ae2eecd210b4eb8bece1217b297ad4ff824b4384c0e3fdd28aaf96edd6135", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "44bec74e2364d491d70f7e42cd0d690922659d329f6465e89feb8a34e8cd3433"},
|
||||||
"ecto_sql": {:hex, :ecto_sql, "3.10.2", "6b98b46534b5c2f8b8b5f03f126e75e2a73c64f3c071149d32987a5378b0fdbd", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.10.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16.0 or ~> 0.17.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "68c018debca57cb9235e3889affdaec7a10616a4e3a80c99fa1d01fdafaa9007"},
|
"ecto_sql": {:hex, :ecto_sql, "3.10.2", "6b98b46534b5c2f8b8b5f03f126e75e2a73c64f3c071149d32987a5378b0fdbd", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.10.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16.0 or ~> 0.17.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "68c018debca57cb9235e3889affdaec7a10616a4e3a80c99fa1d01fdafaa9007"},
|
||||||
|
@ -41,6 +44,7 @@
|
||||||
"telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"},
|
"telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"},
|
||||||
"telemetry_metrics": {:hex, :telemetry_metrics, "0.6.1", "315d9163a1d4660aedc3fee73f33f1d355dcc76c5c3ab3d59e76e3edf80eef1f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7be9e0871c41732c233be71e4be11b96e56177bf15dde64a8ac9ce72ac9834c6"},
|
"telemetry_metrics": {:hex, :telemetry_metrics, "0.6.1", "315d9163a1d4660aedc3fee73f33f1d355dcc76c5c3ab3d59e76e3edf80eef1f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7be9e0871c41732c233be71e4be11b96e56177bf15dde64a8ac9ce72ac9834c6"},
|
||||||
"telemetry_poller": {:hex, :telemetry_poller, "1.0.0", "db91bb424e07f2bb6e73926fcafbfcbcb295f0193e0a00e825e589a0a47e8453", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b3a24eafd66c3f42da30fc3ca7dda1e9d546c12250a2d60d7b81d264fbec4f6e"},
|
"telemetry_poller": {:hex, :telemetry_poller, "1.0.0", "db91bb424e07f2bb6e73926fcafbfcbcb295f0193e0a00e825e589a0a47e8453", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b3a24eafd66c3f42da30fc3ca7dda1e9d546c12250a2d60d7b81d264fbec4f6e"},
|
||||||
|
"text_delta": {:hex, :text_delta, "1.2.0", "392dff17d7b99467afa8cc41435af30c1f642e8ab4df45a68755343a225eb177", [:mix], [], "hexpm", "3437bfb63cf626176baf80040a621f17ff568d17ae46540bf55bbb7cfa6b2438"},
|
||||||
"websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"},
|
"websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"},
|
||||||
"websock_adapter": {:hex, :websock_adapter, "0.5.5", "9dfeee8269b27e958a65b3e235b7e447769f66b5b5925385f5a569269164a210", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "4b977ba4a01918acbf77045ff88de7f6972c2a009213c515a445c48f224ffce9"},
|
"websock_adapter": {:hex, :websock_adapter, "0.5.5", "9dfeee8269b27e958a65b3e235b7e447769f66b5b5925385f5a569269164a210", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "4b977ba4a01918acbf77045ff88de7f6972c2a009213c515a445c48f224ffce9"},
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
defmodule Poex.Repo.Migrations.CreatePadDocuments do
|
||||||
|
use Ecto.Migration
|
||||||
|
|
||||||
|
def change do
|
||||||
|
create table(:pad_documents, primary_key: false) do
|
||||||
|
add :id, :uuid, primary_key: true, null: false
|
||||||
|
add :title, :string
|
||||||
|
add :state, :map, default: "{}"
|
||||||
|
|
||||||
|
timestamps(type: :utc_datetime)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,17 @@
|
||||||
|
defmodule Poex.Repo.Migrations.CreateOperations do
|
||||||
|
use Ecto.Migration
|
||||||
|
|
||||||
|
def change do
|
||||||
|
create table(:operations, primary_key: false) do
|
||||||
|
add :id, :uuid, primary_key: true, null: false
|
||||||
|
add :type, :string
|
||||||
|
add :value, :map
|
||||||
|
add :attributes, :map
|
||||||
|
add :document_id, references(:pad_documents, type: :uuid)
|
||||||
|
|
||||||
|
timestamps()
|
||||||
|
end
|
||||||
|
|
||||||
|
create index(:operations, [:document_id])
|
||||||
|
end
|
||||||
|
end
|
Loading…
Reference in New Issue