Compare commits
85 Commits
Author | SHA1 | Date | |
---|---|---|---|
0c5442f0cd | |||
6c2aba84ef | |||
3e686fa199 | |||
2a8a1d11b8 | |||
c3d066016b | |||
64bf39da29 | |||
c25e02dee1 | |||
5be05ceea6 | |||
e8a041024c | |||
36f385c7f3 | |||
ddb8bbec53 | |||
1e55039a67 | |||
2346a82a46 | |||
b63c6bd318 | |||
b72a79c380 | |||
5cd7a7eef0 | |||
f6dc41498b | |||
1c912a1600 | |||
eeef7c94cd | |||
3c3391b3a6 | |||
52460024b9 | |||
48f7c8d18e | |||
571e0b65b6 | |||
7dc2047e97 | |||
f769e710d8 | |||
d09f698b71 | |||
8666f663ba | |||
22ccea893c | |||
362c406471 | |||
2a87037f06 | |||
53d0dcfb15 | |||
c892b5449b | |||
7cd9dca958 | |||
0e8ddc22c5 | |||
3671ad6199 | |||
7189c955c3 | |||
f56ecc0ba3 | |||
fdfca3f7a5 | |||
c61b2c67b7 | |||
d0d958a638 | |||
a437b5966f | |||
e2378279d7 | |||
1b49b668b3 | |||
03021614b5 | |||
50af86798a | |||
be01723be2 | |||
0a27a4ee29 | |||
e2f8ac6b78 | |||
d5e334dc09 | |||
1d6ba5960c | |||
bc29ca6c20 | |||
bf9fd4880f | |||
957e433847 | |||
edd631f520 | |||
2e1545a9f5 | |||
3e296080f5 | |||
d2ae6024ce | |||
4615a29c11 | |||
eb48ff7dc0 | |||
fcfd9857d5 | |||
c5f96a9d9d | |||
c1a0b4017f | |||
04ebe59afe | |||
50be85a1c3 | |||
994aa96a20 | |||
026bf22f60 | |||
56e6eb3609 | |||
c49140e7f5 | |||
1276635a3e | |||
f00dc50215 | |||
35de8a6395 | |||
96e155a49a | |||
c02fb06eb2 | |||
a9d5649bef | |||
650d61e95f | |||
63d854ffbe | |||
a1c846be33 | |||
1b9f212e66 | |||
7805ddc270 | |||
c1455bccad | |||
dd956be93f | |||
04361a5838 | |||
cb049cb178 | |||
5a41d8b3e7 | |||
64320dbdae |
12
.drone.yml
12
.drone.yml
@ -17,7 +17,7 @@ steps:
|
|||||||
- .mix
|
- .mix
|
||||||
|
|
||||||
- name: test
|
- name: test
|
||||||
image: elixir:1.14.1-alpine
|
image: elixir:1.18.1-alpine
|
||||||
environment:
|
environment:
|
||||||
TEST_DATABASE_URL: ecto://postgres:postgres@database/memex_test
|
TEST_DATABASE_URL: ecto://postgres:postgres@database/memex_test
|
||||||
HOST: testing.example.tld
|
HOST: testing.example.tld
|
||||||
@ -26,8 +26,8 @@ steps:
|
|||||||
MIX_ENV: test
|
MIX_ENV: test
|
||||||
commands:
|
commands:
|
||||||
- apk add --no-cache build-base npm git
|
- apk add --no-cache build-base npm git
|
||||||
- mix local.rebar --force --if-missing
|
- mix local.rebar --force
|
||||||
- mix local.hex --force --if-missing
|
- mix local.hex --force
|
||||||
- mix deps.get
|
- mix deps.get
|
||||||
- npm set cache .npm
|
- npm set cache .npm
|
||||||
- npm --prefix ./assets ci --no-audit --prefer-offline
|
- npm --prefix ./assets ci --no-audit --prefer-offline
|
||||||
@ -42,7 +42,8 @@ steps:
|
|||||||
repo: shibaobun/memex
|
repo: shibaobun/memex
|
||||||
purge: true
|
purge: true
|
||||||
compress: true
|
compress: true
|
||||||
platforms: linux/amd64,linux/arm/v7
|
platforms:
|
||||||
|
- linux/amd64
|
||||||
username:
|
username:
|
||||||
from_secret: docker_username
|
from_secret: docker_username
|
||||||
password:
|
password:
|
||||||
@ -59,7 +60,8 @@ steps:
|
|||||||
repo: shibaobun/memex
|
repo: shibaobun/memex
|
||||||
purge: true
|
purge: true
|
||||||
compress: true
|
compress: true
|
||||||
platforms: linux/amd64,linux/arm/v7
|
platforms:
|
||||||
|
- linux/amd64
|
||||||
username:
|
username:
|
||||||
from_secret: docker_username
|
from_secret: docker_username
|
||||||
password:
|
password:
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
[
|
[
|
||||||
import_deps: [:ecto, :phoenix],
|
import_deps: [:ecto, :ecto_sql, :phoenix],
|
||||||
inputs: ["*.{heex,ex,exs}", "priv/*/seeds.exs", "{config,lib,test}/**/*.{heex,ex,exs}"],
|
inputs: ["*.{heex,ex,exs}", "{config,lib,test}/**/*.{heex,ex,exs}", "priv/*/seeds.exs"],
|
||||||
subdirectories: ["priv/*/migrations"],
|
subdirectories: ["priv/*/migrations"],
|
||||||
plugins: [Phoenix.LiveView.HTMLFormatter]
|
plugins: [Phoenix.LiveView.HTMLFormatter]
|
||||||
]
|
]
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
elixir 1.14.1-otp-25
|
elixir 1.18.1-otp-27
|
||||||
erlang 25.1.2
|
erlang 27.2.1
|
||||||
nodejs 18.9.1
|
nodejs 23.7.0
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
FROM elixir:1.14.1-alpine AS build
|
FROM elixir:1.18.1-alpine AS build
|
||||||
|
|
||||||
# install build dependencies
|
# install build dependencies
|
||||||
RUN apk add --no-cache build-base npm git python3
|
RUN apk add --no-cache build-base npm git python3
|
||||||
@ -7,8 +7,8 @@ RUN apk add --no-cache build-base npm git python3
|
|||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# install hex + rebar
|
# install hex + rebar
|
||||||
RUN mix local.hex --force && \
|
RUN mix local.rebar --force && \
|
||||||
mix local.rebar --force
|
mix local.hex --force
|
||||||
|
|
||||||
# set build ENV
|
# set build ENV
|
||||||
ENV MIX_ENV=prod
|
ENV MIX_ENV=prod
|
||||||
@ -37,7 +37,7 @@ RUN mix do compile, release
|
|||||||
FROM alpine:latest AS app
|
FROM alpine:latest AS app
|
||||||
|
|
||||||
RUN apk upgrade --no-cache && \
|
RUN apk upgrade --no-cache && \
|
||||||
apk add --no-cache bash openssl libssl1.1 libcrypto1.1 libgcc libstdc++ ncurses-libs
|
apk add --no-cache bash openssl libssl3 libcrypto3 libgcc libstdc++ ncurses-libs
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
@import "tailwindcss/base";
|
@use "tailwindcss/base";
|
||||||
@import "tailwindcss/components";
|
@use "tailwindcss/components";
|
||||||
@import "tailwindcss/utilities";
|
@use "tailwindcss/utilities";
|
||||||
|
@use "components" as memex-components;
|
||||||
|
|
||||||
$fa-font-path: "@fortawesome/fontawesome-free/webfonts";
|
$fa-font-path: "@fortawesome/fontawesome-free/webfonts";
|
||||||
@import "@fortawesome/fontawesome-free/scss/fontawesome";
|
@import "@fortawesome/fontawesome-free/scss/fontawesome";
|
||||||
@ -8,8 +9,6 @@ $fa-font-path: "@fortawesome/fontawesome-free/webfonts";
|
|||||||
@import "@fortawesome/fontawesome-free/scss/solid";
|
@import "@fortawesome/fontawesome-free/scss/solid";
|
||||||
@import "@fortawesome/fontawesome-free/scss/brands";
|
@import "@fortawesome/fontawesome-free/scss/brands";
|
||||||
|
|
||||||
@import "components";
|
|
||||||
|
|
||||||
/* fix firefox scrollbars */
|
/* fix firefox scrollbars */
|
||||||
* {
|
* {
|
||||||
scrollbar-width: auto;
|
scrollbar-width: auto;
|
||||||
|
@ -26,16 +26,19 @@ 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 'topbar'
|
import topbar from 'topbar'
|
||||||
|
|
||||||
|
import CtrlEnter from './ctrlenter'
|
||||||
import Date from './date'
|
import Date from './date'
|
||||||
import DateTime from './datetime'
|
import DateTime from './datetime'
|
||||||
import MaintainAttrs from './maintain_attrs'
|
import SanitizeTags from './sanitizetags'
|
||||||
|
import SanitizeTitles from './sanitizetitles'
|
||||||
|
|
||||||
const csrfTokenElement = document.querySelector("meta[name='csrf-token']")
|
const csrfTokenElement = document.querySelector("meta[name='csrf-token']")
|
||||||
let csrfToken
|
let csrfToken
|
||||||
if (csrfTokenElement) { csrfToken = csrfTokenElement.getAttribute('content') }
|
if (csrfTokenElement) { csrfToken = csrfTokenElement.getAttribute('content') }
|
||||||
const liveSocket = new LiveSocket('/live', Socket, {
|
const liveSocket = new LiveSocket('/live', Socket, {
|
||||||
params: { _csrf_token: csrfToken },
|
params: { _csrf_token: csrfToken },
|
||||||
hooks: { Date, DateTime, MaintainAttrs }
|
hooks: { CtrlEnter, Date, DateTime, SanitizeTags, SanitizeTitles }
|
||||||
})
|
})
|
||||||
|
|
||||||
// Show progress bar on live navigation and form submits
|
// Show progress bar on live navigation and form submits
|
||||||
|
11
assets/js/ctrlenter.js
Normal file
11
assets/js/ctrlenter.js
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
export default {
|
||||||
|
addFormSubmit (context) {
|
||||||
|
context.el.addEventListener('keydown', (e) => {
|
||||||
|
if (e.ctrlKey && e.key === 'Enter') {
|
||||||
|
context.el.dispatchEvent(
|
||||||
|
new Event('submit', { bubbles: true, cancelable: true }))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
mounted () { this.addFormSubmit(this) }
|
||||||
|
}
|
@ -1,22 +0,0 @@
|
|||||||
// maintain user adjusted attributes, like textbox length on phoenix liveview
|
|
||||||
// update. https://github.com/phoenixframework/phoenix_live_view/issues/1011
|
|
||||||
|
|
||||||
export default {
|
|
||||||
attrs () {
|
|
||||||
if (this.el && this.el.getAttribute('data-attrs')) {
|
|
||||||
return this.el.getAttribute('data-attrs').split(', ')
|
|
||||||
} else {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
},
|
|
||||||
beforeUpdate () {
|
|
||||||
if (this.el) {
|
|
||||||
this.prevAttrs = this.attrs().map(name => [name, this.el.getAttribute(name)])
|
|
||||||
}
|
|
||||||
},
|
|
||||||
updated () {
|
|
||||||
if (this.el) {
|
|
||||||
this.prevAttrs.forEach(([name, val]) => this.el.setAttribute(name, val))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
11
assets/js/sanitizetags.js
Normal file
11
assets/js/sanitizetags.js
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
export default {
|
||||||
|
SanitizeTags (context) {
|
||||||
|
context.el.addEventListener('keyup', (e) => {
|
||||||
|
e.target.value = e.target.value
|
||||||
|
.replace(' ', ',')
|
||||||
|
.replace(',,', ',')
|
||||||
|
.replace(/[^a-zA-Z0-9,]/, '')
|
||||||
|
})
|
||||||
|
},
|
||||||
|
mounted () { this.SanitizeTags(this) }
|
||||||
|
}
|
10
assets/js/sanitizetitles.js
Normal file
10
assets/js/sanitizetitles.js
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
export default {
|
||||||
|
SanitizeTitles (context) {
|
||||||
|
context.el.addEventListener('keyup', (e) => {
|
||||||
|
e.target.value = e.target.value
|
||||||
|
.replace(' ', '-')
|
||||||
|
.replace(/[^a-zA-Z0-9-]/, '')
|
||||||
|
})
|
||||||
|
},
|
||||||
|
mounted () { this.SanitizeTitles(this) }
|
||||||
|
}
|
23293
assets/package-lock.json
generated
23293
assets/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -3,8 +3,7 @@
|
|||||||
"description": " ",
|
"description": " ",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "v18.9.1",
|
"node": "v23.7.0"
|
||||||
"npm": "8.19.1"
|
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"deploy": "NODE_ENV=production webpack --mode production",
|
"deploy": "NODE_ENV=production webpack --mode production",
|
||||||
@ -13,35 +12,35 @@
|
|||||||
"test": "standard"
|
"test": "standard"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fontsource/nunito-sans": "^4.5.10",
|
"@fontsource/nunito-sans": "^5.1.1",
|
||||||
"@fortawesome/fontawesome-free": "^6.3.0",
|
"@fortawesome/fontawesome-free": "^6.7.2",
|
||||||
"phoenix": "file:../deps/phoenix",
|
"phoenix": "file:../deps/phoenix",
|
||||||
"phoenix_html": "file:../deps/phoenix_html",
|
"phoenix_html": "file:../deps/phoenix_html",
|
||||||
"phoenix_live_view": "file:../deps/phoenix_live_view",
|
"phoenix_live_view": "file:../deps/phoenix_live_view",
|
||||||
"topbar": "^2.0.1"
|
"topbar": "^3.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.21.3",
|
"@babel/core": "^7.26.9",
|
||||||
"@babel/preset-env": "^7.20.2",
|
"@babel/preset-env": "^7.26.9",
|
||||||
"autoprefixer": "^10.4.14",
|
"autoprefixer": "^10.4.20",
|
||||||
"babel-loader": "^9.1.2",
|
"babel-loader": "^9.2.1",
|
||||||
"copy-webpack-plugin": "^11.0.0",
|
"copy-webpack-plugin": "^12.0.2",
|
||||||
"css-loader": "^6.7.3",
|
"css-loader": "^7.1.2",
|
||||||
"css-minimizer-webpack-plugin": "^4.2.2",
|
"css-minimizer-webpack-plugin": "^7.0.0",
|
||||||
"file-loader": "^6.2.0",
|
"file-loader": "^6.2.0",
|
||||||
"mini-css-extract-plugin": "^2.7.5",
|
"mini-css-extract-plugin": "^2.9.2",
|
||||||
"npm-check-updates": "^16.7.12",
|
"npm-check-updates": "^17.1.14",
|
||||||
"postcss": "^8.4.21",
|
"postcss": "^8.5.2",
|
||||||
"postcss-import": "^15.1.0",
|
"postcss-import": "^16.1.0",
|
||||||
"postcss-loader": "^7.1.0",
|
"postcss-loader": "^8.1.1",
|
||||||
"postcss-preset-env": "^8.0.1",
|
"postcss-preset-env": "^10.1.4",
|
||||||
"sass": "^1.59.3",
|
"sass": "^1.85.0",
|
||||||
"sass-loader": "^13.2.1",
|
"sass-loader": "^16.0.5",
|
||||||
"standard": "^17.0.0",
|
"standard": "^17.1.2",
|
||||||
"tailwindcss": "^3.2.7",
|
"tailwindcss": "^3.4.17",
|
||||||
"terser-webpack-plugin": "^5.3.7",
|
"terser-webpack-plugin": "^5.3.11",
|
||||||
"webpack": "^5.76.2",
|
"webpack": "^5.98.0",
|
||||||
"webpack-cli": "^5.0.1",
|
"webpack-cli": "^6.0.1",
|
||||||
"webpack-dev-server": "^4.13.1"
|
"webpack-dev-server": "^5.2.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -45,7 +45,7 @@ module.exports = (env, options) => {
|
|||||||
{
|
{
|
||||||
test: /\.(woff(2)?|ttf|eot|svg|otf)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
|
test: /\.(woff(2)?|ttf|eot|svg|otf)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
|
||||||
type: 'asset/resource',
|
type: 'asset/resource',
|
||||||
generator: { filename: 'fonts/[name][ext]' }
|
generator: { filename: 'fonts/[name].[ext]' }
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
57
changelog.md
57
changelog.md
@ -1,9 +1,66 @@
|
|||||||
|
# v0.1.19
|
||||||
|
- Add backlinks
|
||||||
|
- Fix visibility issues with multiple users
|
||||||
|
|
||||||
|
# v0.1.18
|
||||||
|
- Update deps
|
||||||
|
- Fix content not escaping HTML properly
|
||||||
|
- Add placeholder for empty notes and contexts
|
||||||
|
- Marks some required fields as required
|
||||||
|
|
||||||
|
# v0.1.17
|
||||||
|
- Fix new invite button not working
|
||||||
|
- Fix some descriptions possibly overflowing widths
|
||||||
|
- Update dependencies
|
||||||
|
|
||||||
|
# v0.1.16
|
||||||
|
- Fix empty invite index page
|
||||||
|
- Fix faq copy
|
||||||
|
- Fix issue with emails
|
||||||
|
- Update deps
|
||||||
|
|
||||||
|
# v0.1.15
|
||||||
|
- Sanitize titles while they are being typed
|
||||||
|
- Sanitize tags while they are being typed
|
||||||
|
- Remove requirement for note and content to have content
|
||||||
|
- Prevent possible additional submissions
|
||||||
|
- Fix content being displayed when blank
|
||||||
|
|
||||||
|
# v0.1.14
|
||||||
|
- Fix issue with item content not able to be displayed sometimes
|
||||||
|
|
||||||
|
# v0.1.13
|
||||||
|
- Update dependencies
|
||||||
|
- Fix debounces
|
||||||
|
- Allow space as delimiter for tags
|
||||||
|
- Add bottom padding to page
|
||||||
|
- Make pipeline step not require content
|
||||||
|
- Make content previews resizable
|
||||||
|
- Fix live flashes not dismissable by click
|
||||||
|
- Fix disconnection modal not displaying
|
||||||
|
- Submit items with ctrl-enter
|
||||||
|
- Display backlinks in pipeline description
|
||||||
|
- Modify backlink format
|
||||||
|
|
||||||
|
# v0.1.12
|
||||||
|
- Code quality fixes
|
||||||
|
- Fix error/404 pages not rendering properly
|
||||||
|
- Update dependencies
|
||||||
|
|
||||||
|
# v0.1.11
|
||||||
|
- Update dependencies
|
||||||
|
- ee cummings even more
|
||||||
|
- Improve tests
|
||||||
|
- Change invite path slightly
|
||||||
|
- Disable arm builds since ci fails to build
|
||||||
|
|
||||||
# v0.1.10
|
# v0.1.10
|
||||||
- Improve accessibility
|
- Improve accessibility
|
||||||
- Code quality improvements
|
- Code quality improvements
|
||||||
- Fix dates displaying incorrectly
|
- Fix dates displaying incorrectly
|
||||||
- Add links to readme for github mirror
|
- Add links to readme for github mirror
|
||||||
- Add license (whoops)
|
- Add license (whoops)
|
||||||
|
- Display links in note/context/step contents
|
||||||
|
|
||||||
# v0.1.9
|
# v0.1.9
|
||||||
- Improve server log
|
- Improve server log
|
||||||
|
@ -18,7 +18,10 @@ config :memex, MemexWeb.Endpoint,
|
|||||||
url: [scheme: "https", host: System.get_env("HOST") || "localhost", port: "443"],
|
url: [scheme: "https", host: System.get_env("HOST") || "localhost", port: "443"],
|
||||||
http: [port: String.to_integer(System.get_env("PORT") || "4000")],
|
http: [port: String.to_integer(System.get_env("PORT") || "4000")],
|
||||||
secret_key_base: "KH59P0iZixX5gP/u+zkxxG8vAAj6vgt0YqnwEB5JP5K+E567SsqkCz69uWShjE7I",
|
secret_key_base: "KH59P0iZixX5gP/u+zkxxG8vAAj6vgt0YqnwEB5JP5K+E567SsqkCz69uWShjE7I",
|
||||||
render_errors: [view: MemexWeb.ErrorView, accepts: ~w(html json), layout: false],
|
render_errors: [
|
||||||
|
formats: [html: MemexWeb.ErrorHTML, json: MemexWeb.ErrorJSON],
|
||||||
|
layout: false
|
||||||
|
],
|
||||||
pubsub_server: Memex.PubSub,
|
pubsub_server: Memex.PubSub,
|
||||||
live_view: [signing_salt: "zOLgd3lr"]
|
live_view: [signing_salt: "zOLgd3lr"]
|
||||||
|
|
||||||
|
@ -59,8 +59,7 @@ config :memex, MemexWeb.Endpoint,
|
|||||||
patterns: [
|
patterns: [
|
||||||
~r"priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$",
|
~r"priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$",
|
||||||
~r"priv/gettext/.*(po)$",
|
~r"priv/gettext/.*(po)$",
|
||||||
~r"lib/memex_web/(live|views)/.*(ex)$",
|
~r"lib/memex_web/*/.*(ex)$"
|
||||||
~r"lib/memex_web/templates/.*(eex)$"
|
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -66,7 +66,7 @@ if config_env() == :prod do
|
|||||||
System.get_env("SECRET_KEY_BASE") ||
|
System.get_env("SECRET_KEY_BASE") ||
|
||||||
raise """
|
raise """
|
||||||
environment variable SECRET_KEY_BASE is missing.
|
environment variable SECRET_KEY_BASE is missing.
|
||||||
You can generate one by running: mix phx.gen.secret
|
You can generate one by running: priv/random.sh
|
||||||
"""
|
"""
|
||||||
|
|
||||||
config :memex, MemexWeb.Endpoint, secret_key_base: secret_key_base
|
config :memex, MemexWeb.Endpoint, secret_key_base: secret_key_base
|
||||||
|
@ -9,8 +9,9 @@ config :bcrypt_elixir, :log_rounds, 1
|
|||||||
# to provide built-in test partitioning in CI environment.
|
# to provide built-in test partitioning in CI environment.
|
||||||
# Run `mix help test` for more information.
|
# Run `mix help test` for more information.
|
||||||
config :memex, Memex.Repo,
|
config :memex, Memex.Repo,
|
||||||
|
pool_size: 10,
|
||||||
pool: Ecto.Adapters.SQL.Sandbox,
|
pool: Ecto.Adapters.SQL.Sandbox,
|
||||||
pool_size: 10
|
timeout: 60000
|
||||||
|
|
||||||
# We don't run a server during test. If one is required,
|
# We don't run a server during test. If one is required,
|
||||||
# you can enable the server option below.
|
# you can enable the server option below.
|
||||||
@ -26,10 +27,10 @@ config :memex, Memex.Mailer, adapter: Swoosh.Adapters.Test
|
|||||||
config :memex, Memex.Accounts, registration: "public"
|
config :memex, Memex.Accounts, registration: "public"
|
||||||
|
|
||||||
# Print only warnings and errors during test
|
# Print only warnings and errors during test
|
||||||
config :logger, level: :warn
|
config :logger, level: :warning
|
||||||
|
|
||||||
# Initialize plugs at runtime for faster test compilation
|
# Initialize plugs at runtime for faster test compilation
|
||||||
config :phoenix, :plug_init_mode, :runtime
|
config :phoenix, :plug_init_mode, :runtime
|
||||||
|
|
||||||
# Disable Oban
|
# Disable Oban
|
||||||
config :memex, Oban, queues: false, plugins: false
|
config :memex, Oban, queues: false, plugins: false, testing: :manual
|
||||||
|
@ -113,7 +113,7 @@ In `test` mode (or in the Docker container), memEx will listen for the same envi
|
|||||||
In `prod` mode (or in the Docker container), memEx will listen for the same environment variables as dev mode, but also include the following at runtime:
|
In `prod` mode (or in the Docker container), memEx will listen for the same environment variables as dev mode, but also include the following at runtime:
|
||||||
|
|
||||||
- `SECRET_KEY_BASE`: Secret key base used to sign cookies. Must be generated
|
- `SECRET_KEY_BASE`: Secret key base used to sign cookies. Must be generated
|
||||||
with `docker run -it shibaobun/memex mix phx.gen.secret` and set for server to start.
|
with `docker run -it shibaobun/memex priv/random.sh` and set for server to start.
|
||||||
- `SMTP_HOST`: The url for your SMTP email provider. Must be set
|
- `SMTP_HOST`: The url for your SMTP email provider. Must be set
|
||||||
- `SMTP_PORT`: The port for your SMTP relay. Defaults to `587`.
|
- `SMTP_PORT`: The port for your SMTP relay. Defaults to `587`.
|
||||||
- `SMTP_USERNAME`: The username for your SMTP relay. Must be set!
|
- `SMTP_USERNAME`: The username for your SMTP relay. Must be set!
|
||||||
|
BIN
home.png
BIN
home.png
Binary file not shown.
Before Width: | Height: | Size: 107 KiB After Width: | Height: | Size: 340 KiB |
@ -117,7 +117,7 @@ defmodule Memex.Accounts do
|
|||||||
Multi.new()
|
Multi.new()
|
||||||
|> Multi.one(:users_count, from(u in User, select: count(u.id), distinct: true))
|
|> Multi.one(:users_count, from(u in User, select: count(u.id), distinct: true))
|
||||||
|> Multi.run(:use_invite, fn _changes_so_far, _repo ->
|
|> Multi.run(:use_invite, fn _changes_so_far, _repo ->
|
||||||
if allow_registration?() and invite_token |> is_nil() do
|
if allow_registration?() and !invite_token do
|
||||||
{:ok, nil}
|
{:ok, nil}
|
||||||
else
|
else
|
||||||
Invites.use_invite(invite_token)
|
Invites.use_invite(invite_token)
|
||||||
@ -219,7 +219,7 @@ defmodule Memex.Accounts do
|
|||||||
|
|
||||||
with {:ok, query} <- UserToken.verify_change_email_token_query(token, context),
|
with {:ok, query} <- UserToken.verify_change_email_token_query(token, context),
|
||||||
%UserToken{sent_to: email} <- Repo.one(query),
|
%UserToken{sent_to: email} <- Repo.one(query),
|
||||||
{:ok, _} <- Repo.transaction(user_email_multi(user, email, context)) do
|
{:ok, _result} <- Repo.transaction(user_email_multi(user, email, context)) do
|
||||||
:ok
|
:ok
|
||||||
else
|
else
|
||||||
_error_tuple -> :error
|
_error_tuple -> :error
|
||||||
@ -374,8 +374,8 @@ defmodule Memex.Accounts do
|
|||||||
@doc """
|
@doc """
|
||||||
Deletes the signed token with the given context.
|
Deletes the signed token with the given context.
|
||||||
"""
|
"""
|
||||||
@spec delete_session_token(token :: String.t()) :: :ok
|
@spec delete_user_session_token(token :: String.t()) :: :ok
|
||||||
def delete_session_token(token) do
|
def delete_user_session_token(token) do
|
||||||
UserToken.token_and_context_query(token, "session") |> Repo.delete_all()
|
UserToken.token_and_context_query(token, "session") |> Repo.delete_all()
|
||||||
:ok
|
:ok
|
||||||
end
|
end
|
||||||
@ -405,15 +405,15 @@ defmodule Memex.Accounts do
|
|||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
iex> is_admin?(%User{role: :admin})
|
iex> admin?(%User{role: :admin})
|
||||||
true
|
true
|
||||||
|
|
||||||
iex> is_admin?(%User{})
|
iex> admin?(%User{})
|
||||||
false
|
false
|
||||||
|
|
||||||
"""
|
"""
|
||||||
@spec is_admin?(User.t()) :: boolean()
|
@spec admin?(User.t()) :: boolean()
|
||||||
def is_admin?(%User{id: user_id}) do
|
def admin?(%User{id: user_id}) do
|
||||||
Repo.exists?(from u in User, where: u.id == ^user_id, where: u.role == :admin)
|
Repo.exists?(from u in User, where: u.id == ^user_id, where: u.role == :admin)
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -422,16 +422,16 @@ defmodule Memex.Accounts do
|
|||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
iex> is_already_admin?(%User{role: :admin})
|
iex> already_admin?(%User{role: :admin})
|
||||||
true
|
true
|
||||||
|
|
||||||
iex> is_already_admin?(%User{})
|
iex> already_admin?(%User{})
|
||||||
false
|
false
|
||||||
|
|
||||||
"""
|
"""
|
||||||
@spec is_already_admin?(User.t() | nil) :: boolean()
|
@spec already_admin?(User.t() | nil) :: boolean()
|
||||||
def is_already_admin?(%User{role: :admin}), do: true
|
def already_admin?(%User{role: :admin}), do: true
|
||||||
def is_already_admin?(_invalid_user), do: false
|
def already_admin?(_invalid_user), do: false
|
||||||
|
|
||||||
## Confirmation
|
## Confirmation
|
||||||
|
|
||||||
|
@ -1,48 +0,0 @@
|
|||||||
defmodule Memex.Email do
|
|
||||||
@moduledoc """
|
|
||||||
Emails that can be sent using Swoosh.
|
|
||||||
|
|
||||||
You can find the base email templates at
|
|
||||||
`lib/memex_web/templates/layout/email.html.heex` for html emails and
|
|
||||||
`lib/memex_web/templates/layout/email.txt.heex` for text emails.
|
|
||||||
"""
|
|
||||||
|
|
||||||
use Phoenix.Swoosh, view: MemexWeb.EmailView, layout: {MemexWeb.LayoutView, :email}
|
|
||||||
import MemexWeb.Gettext
|
|
||||||
alias Memex.Accounts.User
|
|
||||||
alias MemexWeb.EmailView
|
|
||||||
|
|
||||||
@typedoc """
|
|
||||||
Represents an HTML and text body email that can be sent
|
|
||||||
"""
|
|
||||||
@type t() :: Swoosh.Email.t()
|
|
||||||
|
|
||||||
@spec base_email(User.t(), String.t()) :: t()
|
|
||||||
defp base_email(%User{email: email}, subject) do
|
|
||||||
from = Application.get_env(:memex, Memex.Mailer)[:email_from] || "noreply@localhost"
|
|
||||||
name = Application.get_env(:memex, Memex.Mailer)[:email_name]
|
|
||||||
new() |> to(email) |> from({name, from}) |> subject(subject)
|
|
||||||
end
|
|
||||||
|
|
||||||
@spec generate_email(key :: String.t(), User.t(), attrs :: map()) :: t()
|
|
||||||
def generate_email("welcome", user, %{"url" => url}) do
|
|
||||||
user
|
|
||||||
|> base_email(dgettext("emails", "Confirm your Memex account"))
|
|
||||||
|> render_body("confirm_email.html", %{user: user, url: url})
|
|
||||||
|> text_body(EmailView.render("confirm_email.txt", %{user: user, url: url}))
|
|
||||||
end
|
|
||||||
|
|
||||||
def generate_email("reset_password", user, %{"url" => url}) do
|
|
||||||
user
|
|
||||||
|> base_email(dgettext("emails", "Reset your Memex password"))
|
|
||||||
|> render_body("reset_password.html", %{user: user, url: url})
|
|
||||||
|> text_body(EmailView.render("reset_password.txt", %{user: user, url: url}))
|
|
||||||
end
|
|
||||||
|
|
||||||
def generate_email("update_email", user, %{"url" => url}) do
|
|
||||||
user
|
|
||||||
|> base_email(dgettext("emails", "Update your Memex email"))
|
|
||||||
|> render_body("update_email.html", %{user: user, url: url})
|
|
||||||
|> text_body(EmailView.render("update_email.txt", %{user: user, url: url}))
|
|
||||||
end
|
|
||||||
end
|
|
@ -4,8 +4,8 @@ defmodule Memex.Accounts.User do
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
use Ecto.Schema
|
use Ecto.Schema
|
||||||
|
use Gettext, backend: MemexWeb.Gettext
|
||||||
import Ecto.Changeset
|
import Ecto.Changeset
|
||||||
import MemexWeb.Gettext
|
|
||||||
alias Ecto.{Association, Changeset, UUID}
|
alias Ecto.{Association, Changeset, UUID}
|
||||||
alias Memex.Accounts.{Invite, User}
|
alias Memex.Accounts.{Invite, User}
|
||||||
|
|
||||||
@ -140,7 +140,7 @@ defmodule Memex.Accounts.User do
|
|||||||
|> cast(attrs, [:email])
|
|> cast(attrs, [:email])
|
||||||
|> validate_email()
|
|> validate_email()
|
||||||
|> case do
|
|> case do
|
||||||
%{changes: %{email: _}} = changeset -> changeset
|
%{changes: %{email: _email}} = changeset -> changeset
|
||||||
%{} = changeset -> add_error(changeset, :email, dgettext("errors", "did not change"))
|
%{} = changeset -> add_error(changeset, :email, dgettext("errors", "did not change"))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -155,7 +155,7 @@ defmodule Memex.Accounts.UserToken do
|
|||||||
from t in __MODULE__, where: t.user_id == ^user.id
|
from t in __MODULE__, where: t.user_id == ^user.id
|
||||||
end
|
end
|
||||||
|
|
||||||
def user_and_contexts_query(user, [_ | _] = contexts) do
|
def user_and_contexts_query(user, [_head | _rest] = contexts) do
|
||||||
from t in __MODULE__, where: t.user_id == ^user.id and t.context in ^contexts
|
from t in __MODULE__, where: t.user_id == ^user.id and t.context in ^contexts
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -22,16 +22,16 @@ defmodule Memex.Contexts do
|
|||||||
@spec list_contexts(search :: String.t() | nil, User.t()) :: [Context.t()]
|
@spec list_contexts(search :: String.t() | nil, User.t()) :: [Context.t()]
|
||||||
def list_contexts(search \\ nil, user)
|
def list_contexts(search \\ nil, user)
|
||||||
|
|
||||||
def list_contexts(search, %{id: user_id}) when search |> is_nil() or search == "" do
|
def list_contexts(search, %{id: user_id}) when user_id |> is_binary() and search in [nil, ""] do
|
||||||
Repo.all(from c in Context, where: c.user_id == ^user_id, order_by: c.slug)
|
Repo.all(from c in Context, order_by: c.slug)
|
||||||
end
|
end
|
||||||
|
|
||||||
def list_contexts(search, %{id: user_id}) when search |> is_binary() do
|
def list_contexts(search, %{id: user_id})
|
||||||
|
when user_id |> is_binary() and search |> is_binary() do
|
||||||
trimmed_search = String.trim(search)
|
trimmed_search = String.trim(search)
|
||||||
|
|
||||||
Repo.all(
|
Repo.all(
|
||||||
from c in Context,
|
from c in Context,
|
||||||
where: c.user_id == ^user_id,
|
|
||||||
where:
|
where:
|
||||||
fragment(
|
fragment(
|
||||||
"search @@ websearch_to_tsquery('english', ?)",
|
"search @@ websearch_to_tsquery('english', ?)",
|
||||||
@ -63,7 +63,7 @@ defmodule Memex.Contexts do
|
|||||||
@spec list_public_contexts(search :: String.t() | nil) :: [Context.t()]
|
@spec list_public_contexts(search :: String.t() | nil) :: [Context.t()]
|
||||||
def list_public_contexts(search \\ nil)
|
def list_public_contexts(search \\ nil)
|
||||||
|
|
||||||
def list_public_contexts(search) when search |> is_nil() or search == "" do
|
def list_public_contexts(search) when search in [nil, ""] do
|
||||||
Repo.all(from c in Context, where: c.visibility == :public, order_by: c.slug)
|
Repo.all(from c in Context, where: c.visibility == :public, order_by: c.slug)
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -88,6 +88,42 @@ defmodule Memex.Contexts do
|
|||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Returns the list of contexts that link to a particular slug.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
iex> backlink(%User{id: 123})
|
||||||
|
[%Context{}, ...]
|
||||||
|
|
||||||
|
iex> backlink("[other-context]", %User{id: 123})
|
||||||
|
[%Context{content: "[other-context]"}, ...]
|
||||||
|
|
||||||
|
"""
|
||||||
|
@spec backlink(String.t(), User.t()) :: [Context.t()]
|
||||||
|
def backlink(link, %{id: user_id}) when user_id |> is_binary() do
|
||||||
|
link = link |> String.replace("[", "\\[") |> String.replace("]", "\\]")
|
||||||
|
link_regex = "(^|[^\[])#{link}($|[^\]])"
|
||||||
|
|
||||||
|
Repo.all(
|
||||||
|
from c in Context,
|
||||||
|
where: fragment("? ~ ?", c.content, ^link_regex),
|
||||||
|
order_by: c.slug
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def backlink(link, _invalid_user) do
|
||||||
|
link = link |> String.replace("[", "\\[") |> String.replace("]", "\\]")
|
||||||
|
link_regex = "(^|[^\[])#{link}($|[^\]])"
|
||||||
|
|
||||||
|
Repo.all(
|
||||||
|
from c in Context,
|
||||||
|
where: fragment("? ~ ?", c.content, ^link_regex),
|
||||||
|
where: c.visibility == :public,
|
||||||
|
order_by: c.slug
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Gets a single context.
|
Gets a single context.
|
||||||
|
|
||||||
@ -103,12 +139,8 @@ defmodule Memex.Contexts do
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
@spec get_context!(Context.id(), User.t()) :: Context.t()
|
@spec get_context!(Context.id(), User.t()) :: Context.t()
|
||||||
def get_context!(id, %{id: user_id}) do
|
def get_context!(id, %{id: user_id}) when user_id |> is_binary() do
|
||||||
Repo.one!(
|
Repo.one!(from c in Context, where: c.id == ^id)
|
||||||
from c in Context,
|
|
||||||
where: c.id == ^id,
|
|
||||||
where: c.user_id == ^user_id or c.visibility in [:public, :unlisted]
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def get_context!(id, _invalid_user) do
|
def get_context!(id, _invalid_user) do
|
||||||
@ -134,12 +166,8 @@ defmodule Memex.Contexts do
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
@spec get_context_by_slug(Context.slug(), User.t()) :: Context.t() | nil
|
@spec get_context_by_slug(Context.slug(), User.t()) :: Context.t() | nil
|
||||||
def get_context_by_slug(slug, %{id: user_id}) do
|
def get_context_by_slug(slug, %{id: user_id}) when user_id |> is_binary() do
|
||||||
Repo.one(
|
Repo.one(from c in Context, where: c.slug == ^slug)
|
||||||
from c in Context,
|
|
||||||
where: c.slug == ^slug,
|
|
||||||
where: c.user_id == ^user_id or c.visibility in [:public, :unlisted]
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def get_context_by_slug(slug, _invalid_user) do
|
def get_context_by_slug(slug, _invalid_user) do
|
||||||
@ -194,23 +222,16 @@ defmodule Memex.Contexts do
|
|||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
iex> delete_context(%Context{user_id: 123}, %User{id: 123})
|
|
||||||
{:ok, %Context{}}
|
|
||||||
|
|
||||||
iex> delete_context(%Context{user_id: 123}, %User{role: :admin})
|
|
||||||
{:ok, %Context{}}
|
|
||||||
|
|
||||||
iex> delete_context(%Context{}, %User{id: 123})
|
iex> delete_context(%Context{}, %User{id: 123})
|
||||||
|
{:ok, %Context{}}
|
||||||
|
|
||||||
|
iex> delete_context(%Context{}, nil)
|
||||||
{:error, %Ecto.Changeset{}}
|
{:error, %Ecto.Changeset{}}
|
||||||
|
|
||||||
"""
|
"""
|
||||||
@spec delete_context(Context.t(), User.t()) ::
|
@spec delete_context(Context.t(), User.t()) ::
|
||||||
{:ok, Context.t()} | {:error, Context.changeset()}
|
{:ok, Context.t()} | {:error, Context.changeset()}
|
||||||
def delete_context(%Context{user_id: user_id} = context, %{id: user_id}) do
|
def delete_context(%Context{} = context, %{id: user_id}) when user_id |> is_binary() do
|
||||||
context |> Repo.delete()
|
|
||||||
end
|
|
||||||
|
|
||||||
def delete_context(%Context{} = context, %{role: :admin}) do
|
|
||||||
context |> Repo.delete()
|
context |> Repo.delete()
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -228,13 +249,4 @@ defmodule Memex.Contexts do
|
|||||||
def change_context(%Context{} = context, attrs \\ %{}, user) do
|
def change_context(%Context{} = context, attrs \\ %{}, user) do
|
||||||
context |> Context.update_changeset(attrs, user)
|
context |> Context.update_changeset(attrs, user)
|
||||||
end
|
end
|
||||||
|
|
||||||
@spec is_owner_or_admin?(Context.t(), User.t()) :: boolean()
|
|
||||||
def is_owner_or_admin?(%{user_id: user_id}, %{id: user_id}), do: true
|
|
||||||
def is_owner_or_admin?(_context, %{role: :admin}), do: true
|
|
||||||
def is_owner_or_admin?(_context, _other_user), do: false
|
|
||||||
|
|
||||||
@spec is_owner?(Context.t(), User.t()) :: boolean()
|
|
||||||
def is_owner?(%{user_id: user_id}, %{id: user_id}), do: true
|
|
||||||
def is_owner?(_context, _other_user), do: false
|
|
||||||
end
|
end
|
||||||
|
@ -4,11 +4,12 @@ defmodule Memex.Contexts.Context do
|
|||||||
into a single consideration
|
into a single consideration
|
||||||
"""
|
"""
|
||||||
use Ecto.Schema
|
use Ecto.Schema
|
||||||
|
use Gettext, backend: MemexWeb.Gettext
|
||||||
import Ecto.Changeset
|
import Ecto.Changeset
|
||||||
import MemexWeb.Gettext
|
|
||||||
alias Ecto.{Changeset, UUID}
|
alias Ecto.{Changeset, UUID}
|
||||||
alias Memex.{Accounts.User, Repo}
|
alias Memex.{Accounts.User, Repo}
|
||||||
|
|
||||||
|
@derive {Phoenix.Param, key: :slug}
|
||||||
@derive {Jason.Encoder,
|
@derive {Jason.Encoder,
|
||||||
only: [
|
only: [
|
||||||
:slug,
|
:slug,
|
||||||
@ -56,20 +57,21 @@ defmodule Memex.Contexts.Context do
|
|||||||
|> validate_format(:slug, ~r/^[\p{L}\p{N}\-]+$/,
|
|> validate_format(:slug, ~r/^[\p{L}\p{N}\-]+$/,
|
||||||
message: dgettext("errors", "invalid format: only numbers, letters and hyphen are accepted")
|
message: dgettext("errors", "invalid format: only numbers, letters and hyphen are accepted")
|
||||||
)
|
)
|
||||||
|> validate_required([:slug, :content, :user_id, :visibility])
|
|> validate_required([:slug, :user_id, :visibility])
|
||||||
|> unique_constraint(:slug)
|
|> unique_constraint(:slug)
|
||||||
|> unsafe_validate_unique(:slug, Repo)
|
|> unsafe_validate_unique(:slug, Repo)
|
||||||
end
|
end
|
||||||
|
|
||||||
@spec update_changeset(t(), attrs :: map(), User.t()) :: changeset()
|
@spec update_changeset(t(), attrs :: map(), User.t()) :: changeset()
|
||||||
def update_changeset(%{user_id: user_id} = note, attrs, %User{id: user_id}) do
|
def update_changeset(%__MODULE__{} = context, attrs, %User{id: user_id})
|
||||||
note
|
when user_id |> is_binary() do
|
||||||
|
context
|
||||||
|> cast(attrs, [:slug, :content, :tags, :visibility])
|
|> cast(attrs, [:slug, :content, :tags, :visibility])
|
||||||
|> cast_tags_string(attrs)
|
|> cast_tags_string(attrs)
|
||||||
|> validate_format(:slug, ~r/^[\p{L}\p{N}\-]+$/,
|
|> validate_format(:slug, ~r/^[\p{L}\p{N}\-]+$/,
|
||||||
message: dgettext("errors", "invalid format: only numbers, letters and hyphen are accepted")
|
message: dgettext("errors", "invalid format: only numbers, letters and hyphen are accepted")
|
||||||
)
|
)
|
||||||
|> validate_required([:slug, :content, :visibility])
|
|> validate_required([:slug, :visibility])
|
||||||
|> unique_constraint(:slug)
|
|> unique_constraint(:slug)
|
||||||
|> unsafe_validate_unique(:slug, Repo)
|
|> unsafe_validate_unique(:slug, Repo)
|
||||||
end
|
end
|
||||||
@ -78,11 +80,11 @@ defmodule Memex.Contexts.Context do
|
|||||||
changeset
|
changeset
|
||||||
|> put_change(:tags_string, changeset |> get_field(:tags) |> get_tags_string())
|
|> put_change(:tags_string, changeset |> get_field(:tags) |> get_tags_string())
|
||||||
|> cast(attrs, [:tags_string])
|
|> cast(attrs, [:tags_string])
|
||||||
|> validate_format(:tags_string, ~r/^[\p{L}\p{N}\-\,]+$/,
|
|> validate_format(:tags_string, ~r/^[\p{L}\p{N}\-\, ]+$/,
|
||||||
message:
|
message:
|
||||||
dgettext(
|
dgettext(
|
||||||
"errors",
|
"errors",
|
||||||
"invalid format: only numbers, letters and hyphen are accepted. tags must be comma-delimited"
|
"invalid format: only numbers, letters and hyphen are accepted. tags must be comma or space delimited"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|> cast_tags()
|
|> cast_tags()
|
||||||
@ -97,9 +99,9 @@ defmodule Memex.Contexts.Context do
|
|||||||
|
|
||||||
defp process_tags(tags_string) when tags_string |> is_binary() do
|
defp process_tags(tags_string) when tags_string |> is_binary() do
|
||||||
tags_string
|
tags_string
|
||||||
|> String.split(",", trim: true)
|
|> String.split([",", " "], trim: true)
|
||||||
|> Enum.map(fn str -> str |> String.trim() end)
|
|> Enum.map(fn str -> str |> String.trim() end)
|
||||||
|> Enum.reject(fn str -> str |> is_nil() end)
|
|> Enum.reject(fn str -> str in [nil, ""] end)
|
||||||
|> Enum.sort()
|
|> Enum.sort()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
56
lib/memex/email.ex
Normal file
56
lib/memex/email.ex
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
defmodule Memex.Email do
|
||||||
|
@moduledoc """
|
||||||
|
Emails that can be sent using Swoosh.
|
||||||
|
|
||||||
|
You can find the base email templates at
|
||||||
|
`lib/memex_web/components/layouts/email_html.html.heex` for html emails and
|
||||||
|
`lib/memex_web/components/layouts/email_text.txt.eex` for text emails.
|
||||||
|
"""
|
||||||
|
|
||||||
|
use Gettext, backend: MemexWeb.Gettext
|
||||||
|
import Swoosh.Email
|
||||||
|
import Phoenix.Template
|
||||||
|
alias Memex.Accounts.User
|
||||||
|
alias MemexWeb.{EmailHTML, Layouts}
|
||||||
|
|
||||||
|
@typedoc """
|
||||||
|
Represents an HTML and text body email that can be sent
|
||||||
|
"""
|
||||||
|
@type t() :: Swoosh.Email.t()
|
||||||
|
|
||||||
|
@spec base_email(User.t(), String.t()) :: t()
|
||||||
|
defp base_email(%User{email: email}, subject) do
|
||||||
|
from = Application.get_env(:memex, Memex.Mailer)[:email_from] || "noreply@localhost"
|
||||||
|
name = Application.get_env(:memex, Memex.Mailer)[:email_name]
|
||||||
|
new() |> to(email) |> from({name, from}) |> subject(subject)
|
||||||
|
end
|
||||||
|
|
||||||
|
@spec generate_email(key :: String.t(), User.t(), attrs :: map()) :: t()
|
||||||
|
def generate_email("welcome", user, %{"url" => url}) do
|
||||||
|
user
|
||||||
|
|> base_email(dgettext("emails", "confirm your memEx account"))
|
||||||
|
|> render_body(:confirm_email, %{user: user, url: url})
|
||||||
|
end
|
||||||
|
|
||||||
|
def generate_email("reset_password", user, %{"url" => url}) do
|
||||||
|
user
|
||||||
|
|> base_email(dgettext("emails", "reset your memEx password"))
|
||||||
|
|> render_body(:reset_password, %{user: user, url: url})
|
||||||
|
end
|
||||||
|
|
||||||
|
def generate_email("update_email", user, %{"url" => url}) do
|
||||||
|
user
|
||||||
|
|> base_email(dgettext("emails", "update your memEx email"))
|
||||||
|
|> render_body(:update_email, %{user: user, url: url})
|
||||||
|
end
|
||||||
|
|
||||||
|
defp render_body(email, template, assigns) do
|
||||||
|
html_heex = apply(EmailHTML, String.to_existing_atom("#{template}_html"), [assigns])
|
||||||
|
html = render_to_string(Layouts, "email_html", "html", email: email, inner_content: html_heex)
|
||||||
|
|
||||||
|
text_heex = apply(EmailHTML, String.to_existing_atom("#{template}_text"), [assigns])
|
||||||
|
text = render_to_string(Layouts, "email_text", "text", email: email, inner_content: text_heex)
|
||||||
|
|
||||||
|
email |> html_body(html) |> text_body(text)
|
||||||
|
end
|
||||||
|
end
|
@ -14,17 +14,17 @@ defmodule Memex.Logger do
|
|||||||
|> Map.put(:stacktrace, Exception.format_stacktrace(stacktrace))
|
|> Map.put(:stacktrace, Exception.format_stacktrace(stacktrace))
|
||||||
|> pretty_encode()
|
|> pretty_encode()
|
||||||
|
|
||||||
Logger.error(meta.reason, data: data)
|
Logger.error("#{meta.reason} #{data}")
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_event([:oban, :job, :start], measure, meta, _config) do
|
def handle_event([:oban, :job, :start], measure, meta, _config) do
|
||||||
data = get_oban_job_data(meta, measure) |> pretty_encode()
|
data = get_oban_job_data(meta, measure) |> pretty_encode()
|
||||||
Logger.info("Started oban job", data: data)
|
Logger.info("Started oban job: #{data}")
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_event([:oban, :job, :stop], measure, meta, _config) do
|
def handle_event([:oban, :job, :stop], measure, meta, _config) do
|
||||||
data = get_oban_job_data(meta, measure) |> pretty_encode()
|
data = get_oban_job_data(meta, measure) |> pretty_encode()
|
||||||
Logger.info("Finished oban job", data: data)
|
Logger.info("Finished oban job: #{data}")
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_event([:oban, :job, unhandled_event], measure, meta, _config) do
|
def handle_event([:oban, :job, unhandled_event], measure, meta, _config) do
|
||||||
@ -33,7 +33,7 @@ defmodule Memex.Logger do
|
|||||||
|> Map.put(:event, unhandled_event)
|
|> Map.put(:event, unhandled_event)
|
||||||
|> pretty_encode()
|
|> pretty_encode()
|
||||||
|
|
||||||
Logger.warning("Unhandled oban job event", data: data)
|
Logger.warning("Unhandled oban job event: #{data}")
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_event(unhandled_event, measure, meta, config) do
|
def handle_event(unhandled_event, measure, meta, config) do
|
||||||
@ -45,7 +45,7 @@ defmodule Memex.Logger do
|
|||||||
config: config
|
config: config
|
||||||
})
|
})
|
||||||
|
|
||||||
Logger.warning("Unhandled telemetry event", data: data)
|
Logger.warning("Unhandled telemetry event: #{data}")
|
||||||
end
|
end
|
||||||
|
|
||||||
defp get_oban_job_data(%{job: job}, measure) do
|
defp get_oban_job_data(%{job: job}, measure) do
|
||||||
|
@ -22,16 +22,15 @@ defmodule Memex.Notes do
|
|||||||
@spec list_notes(search :: String.t() | nil, User.t()) :: [Note.t()]
|
@spec list_notes(search :: String.t() | nil, User.t()) :: [Note.t()]
|
||||||
def list_notes(search \\ nil, user)
|
def list_notes(search \\ nil, user)
|
||||||
|
|
||||||
def list_notes(search, %{id: user_id}) when search |> is_nil() or search == "" do
|
def list_notes(search, %{id: user_id}) when user_id |> is_binary() and search in [nil, ""] do
|
||||||
Repo.all(from n in Note, where: n.user_id == ^user_id, order_by: n.slug)
|
Repo.all(from n in Note, order_by: n.slug)
|
||||||
end
|
end
|
||||||
|
|
||||||
def list_notes(search, %{id: user_id}) when search |> is_binary() do
|
def list_notes(search, %{id: user_id}) when user_id |> is_binary() and search |> is_binary() do
|
||||||
trimmed_search = String.trim(search)
|
trimmed_search = String.trim(search)
|
||||||
|
|
||||||
Repo.all(
|
Repo.all(
|
||||||
from n in Note,
|
from n in Note,
|
||||||
where: n.user_id == ^user_id,
|
|
||||||
where:
|
where:
|
||||||
fragment(
|
fragment(
|
||||||
"search @@ websearch_to_tsquery('english', ?)",
|
"search @@ websearch_to_tsquery('english', ?)",
|
||||||
@ -62,7 +61,7 @@ defmodule Memex.Notes do
|
|||||||
@spec list_public_notes(search :: String.t() | nil) :: [Note.t()]
|
@spec list_public_notes(search :: String.t() | nil) :: [Note.t()]
|
||||||
def list_public_notes(search \\ nil)
|
def list_public_notes(search \\ nil)
|
||||||
|
|
||||||
def list_public_notes(search) when search |> is_nil() or search == "" do
|
def list_public_notes(search) when search in [nil, ""] do
|
||||||
Repo.all(from n in Note, where: n.visibility == :public, order_by: n.slug)
|
Repo.all(from n in Note, where: n.visibility == :public, order_by: n.slug)
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -87,6 +86,42 @@ defmodule Memex.Notes do
|
|||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Returns the list of notes that link to a particular slug.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
iex> backlink(%User{id: 123})
|
||||||
|
[%Note{}, ...]
|
||||||
|
|
||||||
|
iex> backlink("[other-note]", %User{id: 123})
|
||||||
|
[%Note{content: "[other-note]"}, ...]
|
||||||
|
|
||||||
|
"""
|
||||||
|
@spec backlink(String.t(), User.t()) :: [Note.t()]
|
||||||
|
def backlink(link, %{id: user_id}) when user_id |> is_binary() do
|
||||||
|
link = link |> String.replace("[", "\\[") |> String.replace("]", "\\]")
|
||||||
|
link_regex = "(^|[^\[])#{link}($|[^\]])"
|
||||||
|
|
||||||
|
Repo.all(
|
||||||
|
from n in Note,
|
||||||
|
where: fragment("? ~ ?", n.content, ^link_regex),
|
||||||
|
order_by: n.slug
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def backlink(link, _invalid_user) do
|
||||||
|
link = link |> String.replace("[", "\\[") |> String.replace("]", "\\]")
|
||||||
|
link_regex = "(^|[^\[])#{link}($|[^\]])"
|
||||||
|
|
||||||
|
Repo.all(
|
||||||
|
from n in Note,
|
||||||
|
where: fragment("? ~ ?", n.content, ^link_regex),
|
||||||
|
where: n.visibility == :public,
|
||||||
|
order_by: n.slug
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Gets a single note.
|
Gets a single note.
|
||||||
|
|
||||||
@ -102,12 +137,8 @@ defmodule Memex.Notes do
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
@spec get_note!(Note.id(), User.t()) :: Note.t()
|
@spec get_note!(Note.id(), User.t()) :: Note.t()
|
||||||
def get_note!(id, %{id: user_id}) do
|
def get_note!(id, %{id: user_id}) when user_id |> is_binary() do
|
||||||
Repo.one!(
|
Repo.one!(from n in Note, where: n.id == ^id)
|
||||||
from n in Note,
|
|
||||||
where: n.id == ^id,
|
|
||||||
where: n.user_id == ^user_id or n.visibility in [:public, :unlisted]
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def get_note!(id, _invalid_user) do
|
def get_note!(id, _invalid_user) do
|
||||||
@ -133,12 +164,8 @@ defmodule Memex.Notes do
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
@spec get_note_by_slug(Note.slug(), User.t()) :: Note.t() | nil
|
@spec get_note_by_slug(Note.slug(), User.t()) :: Note.t() | nil
|
||||||
def get_note_by_slug(slug, %{id: user_id}) do
|
def get_note_by_slug(slug, %{id: user_id}) when user_id |> is_binary() do
|
||||||
Repo.one(
|
Repo.one(from n in Note, where: n.slug == ^slug)
|
||||||
from n in Note,
|
|
||||||
where: n.slug == ^slug,
|
|
||||||
where: n.user_id == ^user_id or n.visibility in [:public, :unlisted]
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def get_note_by_slug(slug, _invalid_user) do
|
def get_note_by_slug(slug, _invalid_user) do
|
||||||
@ -192,22 +219,15 @@ defmodule Memex.Notes do
|
|||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
iex> delete_note(%Note{user_id: 123}, %User{id: 123})
|
|
||||||
{:ok, %Note{}}
|
|
||||||
|
|
||||||
iex> delete_note(%Note{}, %User{role: :admin})
|
|
||||||
{:ok, %Note{}}
|
|
||||||
|
|
||||||
iex> delete_note(%Note{}, %User{id: 123})
|
iex> delete_note(%Note{}, %User{id: 123})
|
||||||
|
{:ok, %Note{}}
|
||||||
|
|
||||||
|
iex> delete_note(%Note{}, nil)
|
||||||
{:error, %Ecto.Changeset{}}
|
{:error, %Ecto.Changeset{}}
|
||||||
|
|
||||||
"""
|
"""
|
||||||
@spec delete_note(Note.t(), User.t()) :: {:ok, Note.t()} | {:error, Note.changeset()}
|
@spec delete_note(Note.t(), User.t()) :: {:ok, Note.t()} | {:error, Note.changeset()}
|
||||||
def delete_note(%Note{user_id: user_id} = note, %{id: user_id}) do
|
def delete_note(%Note{} = note, %{id: user_id}) when user_id |> is_binary() do
|
||||||
note |> Repo.delete()
|
|
||||||
end
|
|
||||||
|
|
||||||
def delete_note(%Note{} = note, %{role: :admin}) do
|
|
||||||
note |> Repo.delete()
|
note |> Repo.delete()
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -228,13 +248,4 @@ defmodule Memex.Notes do
|
|||||||
def change_note(%Note{} = note, attrs \\ %{}, user) do
|
def change_note(%Note{} = note, attrs \\ %{}, user) do
|
||||||
note |> Note.update_changeset(attrs, user)
|
note |> Note.update_changeset(attrs, user)
|
||||||
end
|
end
|
||||||
|
|
||||||
@spec is_owner_or_admin?(Note.t(), User.t()) :: boolean()
|
|
||||||
def is_owner_or_admin?(%{user_id: user_id}, %{id: user_id}), do: true
|
|
||||||
def is_owner_or_admin?(_context, %{role: :admin}), do: true
|
|
||||||
def is_owner_or_admin?(_context, _other_user), do: false
|
|
||||||
|
|
||||||
@spec is_owner?(Note.t(), User.t()) :: boolean()
|
|
||||||
def is_owner?(%{user_id: user_id}, %{id: user_id}), do: true
|
|
||||||
def is_owner?(_context, _other_user), do: false
|
|
||||||
end
|
end
|
||||||
|
@ -3,11 +3,12 @@ defmodule Memex.Notes.Note do
|
|||||||
Schema for a user-written note
|
Schema for a user-written note
|
||||||
"""
|
"""
|
||||||
use Ecto.Schema
|
use Ecto.Schema
|
||||||
|
use Gettext, backend: MemexWeb.Gettext
|
||||||
import Ecto.Changeset
|
import Ecto.Changeset
|
||||||
import MemexWeb.Gettext
|
|
||||||
alias Ecto.{Changeset, UUID}
|
alias Ecto.{Changeset, UUID}
|
||||||
alias Memex.{Accounts.User, Repo}
|
alias Memex.{Accounts.User, Repo}
|
||||||
|
|
||||||
|
@derive {Phoenix.Param, key: :slug}
|
||||||
@derive {Jason.Encoder,
|
@derive {Jason.Encoder,
|
||||||
only: [
|
only: [
|
||||||
:slug,
|
:slug,
|
||||||
@ -55,20 +56,21 @@ defmodule Memex.Notes.Note do
|
|||||||
|> validate_format(:slug, ~r/^[\p{L}\p{N}\-]+$/,
|
|> validate_format(:slug, ~r/^[\p{L}\p{N}\-]+$/,
|
||||||
message: dgettext("errors", "invalid format: only numbers, letters and hyphen are accepted")
|
message: dgettext("errors", "invalid format: only numbers, letters and hyphen are accepted")
|
||||||
)
|
)
|
||||||
|> validate_required([:slug, :content, :user_id, :visibility])
|
|> validate_required([:slug, :user_id, :visibility])
|
||||||
|> unique_constraint(:slug)
|
|> unique_constraint(:slug)
|
||||||
|> unsafe_validate_unique(:slug, Repo)
|
|> unsafe_validate_unique(:slug, Repo)
|
||||||
end
|
end
|
||||||
|
|
||||||
@spec update_changeset(t(), attrs :: map(), User.t()) :: changeset()
|
@spec update_changeset(t(), attrs :: map(), User.t()) :: changeset()
|
||||||
def update_changeset(%{user_id: user_id} = note, attrs, %User{id: user_id}) do
|
def update_changeset(%__MODULE__{} = note, attrs, %User{id: user_id})
|
||||||
|
when user_id |> is_binary() do
|
||||||
note
|
note
|
||||||
|> cast(attrs, [:slug, :content, :tags, :visibility])
|
|> cast(attrs, [:slug, :content, :tags, :visibility])
|
||||||
|> cast_tags_string(attrs)
|
|> cast_tags_string(attrs)
|
||||||
|> validate_format(:slug, ~r/^[\p{L}\p{N}\-]+$/,
|
|> validate_format(:slug, ~r/^[\p{L}\p{N}\-]+$/,
|
||||||
message: dgettext("errors", "invalid format: only numbers, letters and hyphen are accepted")
|
message: dgettext("errors", "invalid format: only numbers, letters and hyphen are accepted")
|
||||||
)
|
)
|
||||||
|> validate_required([:slug, :content, :visibility])
|
|> validate_required([:slug, :visibility])
|
||||||
|> unique_constraint(:slug)
|
|> unique_constraint(:slug)
|
||||||
|> unsafe_validate_unique(:slug, Repo)
|
|> unsafe_validate_unique(:slug, Repo)
|
||||||
end
|
end
|
||||||
@ -77,11 +79,11 @@ defmodule Memex.Notes.Note do
|
|||||||
changeset
|
changeset
|
||||||
|> put_change(:tags_string, changeset |> get_field(:tags) |> get_tags_string())
|
|> put_change(:tags_string, changeset |> get_field(:tags) |> get_tags_string())
|
||||||
|> cast(attrs, [:tags_string])
|
|> cast(attrs, [:tags_string])
|
||||||
|> validate_format(:tags_string, ~r/^[\p{L}\p{N}\-\,]+$/,
|
|> validate_format(:tags_string, ~r/^[\p{L}\p{N}\-\, ]+$/,
|
||||||
message:
|
message:
|
||||||
dgettext(
|
dgettext(
|
||||||
"errors",
|
"errors",
|
||||||
"invalid format: only numbers, letters and hyphen are accepted. tags must be comma-delimited"
|
"invalid format: only numbers, letters and hyphen are accepted. tags must be comma or space delimited"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|> cast_tags()
|
|> cast_tags()
|
||||||
@ -96,9 +98,9 @@ defmodule Memex.Notes.Note do
|
|||||||
|
|
||||||
defp process_tags(tags_string) when tags_string |> is_binary() do
|
defp process_tags(tags_string) when tags_string |> is_binary() do
|
||||||
tags_string
|
tags_string
|
||||||
|> String.split(",", trim: true)
|
|> String.split([",", " "], trim: true)
|
||||||
|> Enum.map(fn str -> str |> String.trim() end)
|
|> Enum.map(fn str -> str |> String.trim() end)
|
||||||
|> Enum.reject(fn str -> str |> is_nil() end)
|
|> Enum.reject(fn str -> str in [nil, ""] end)
|
||||||
|> Enum.sort()
|
|> Enum.sort()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -22,16 +22,17 @@ defmodule Memex.Pipelines do
|
|||||||
@spec list_pipelines(search :: String.t() | nil, User.t()) :: [Pipeline.t()]
|
@spec list_pipelines(search :: String.t() | nil, User.t()) :: [Pipeline.t()]
|
||||||
def list_pipelines(search \\ nil, user)
|
def list_pipelines(search \\ nil, user)
|
||||||
|
|
||||||
def list_pipelines(search, %{id: user_id}) when search |> is_nil() or search == "" do
|
def list_pipelines(search, %{id: user_id})
|
||||||
Repo.all(from p in Pipeline, where: p.user_id == ^user_id, order_by: p.slug)
|
when user_id |> is_binary() and search in [nil, ""] do
|
||||||
|
Repo.all(from p in Pipeline, order_by: p.slug)
|
||||||
end
|
end
|
||||||
|
|
||||||
def list_pipelines(search, %{id: user_id}) when search |> is_binary() do
|
def list_pipelines(search, %{id: user_id})
|
||||||
|
when user_id |> is_binary() and search |> is_binary() do
|
||||||
trimmed_search = String.trim(search)
|
trimmed_search = String.trim(search)
|
||||||
|
|
||||||
Repo.all(
|
Repo.all(
|
||||||
from p in Pipeline,
|
from p in Pipeline,
|
||||||
where: p.user_id == ^user_id,
|
|
||||||
where:
|
where:
|
||||||
fragment(
|
fragment(
|
||||||
"search @@ websearch_to_tsquery('english', ?)",
|
"search @@ websearch_to_tsquery('english', ?)",
|
||||||
@ -62,7 +63,7 @@ defmodule Memex.Pipelines do
|
|||||||
@spec list_public_pipelines(search :: String.t() | nil) :: [Pipeline.t()]
|
@spec list_public_pipelines(search :: String.t() | nil) :: [Pipeline.t()]
|
||||||
def list_public_pipelines(search \\ nil)
|
def list_public_pipelines(search \\ nil)
|
||||||
|
|
||||||
def list_public_pipelines(search) when search |> is_nil() or search == "" do
|
def list_public_pipelines(search) when search in [nil, ""] do
|
||||||
Repo.all(from p in Pipeline, where: p.visibility == :public, order_by: p.slug)
|
Repo.all(from p in Pipeline, where: p.visibility == :public, order_by: p.slug)
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -102,12 +103,8 @@ defmodule Memex.Pipelines do
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
@spec get_pipeline!(Pipeline.id(), User.t()) :: Pipeline.t()
|
@spec get_pipeline!(Pipeline.id(), User.t()) :: Pipeline.t()
|
||||||
def get_pipeline!(id, %{id: user_id}) do
|
def get_pipeline!(id, %{id: user_id}) when user_id |> is_binary() do
|
||||||
Repo.one!(
|
Repo.one!(from p in Pipeline, where: p.id == ^id)
|
||||||
from p in Pipeline,
|
|
||||||
where: p.id == ^id,
|
|
||||||
where: p.user_id == ^user_id or p.visibility in [:public, :unlisted]
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def get_pipeline!(id, _invalid_user) do
|
def get_pipeline!(id, _invalid_user) do
|
||||||
@ -133,12 +130,8 @@ defmodule Memex.Pipelines do
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
@spec get_pipeline_by_slug(Pipeline.slug(), User.t()) :: Pipeline.t() | nil
|
@spec get_pipeline_by_slug(Pipeline.slug(), User.t()) :: Pipeline.t() | nil
|
||||||
def get_pipeline_by_slug(slug, %{id: user_id}) do
|
def get_pipeline_by_slug(slug, %{id: user_id}) when user_id |> is_binary() do
|
||||||
Repo.one(
|
Repo.one(from p in Pipeline, where: p.slug == ^slug)
|
||||||
from p in Pipeline,
|
|
||||||
where: p.slug == ^slug,
|
|
||||||
where: p.user_id == ^user_id or p.visibility in [:public, :unlisted]
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def get_pipeline_by_slug(slug, _invalid_user) do
|
def get_pipeline_by_slug(slug, _invalid_user) do
|
||||||
@ -149,6 +142,50 @@ defmodule Memex.Pipelines do
|
|||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Returns the list of pipelines that link to a particular slug.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
iex> backlink(%User{id: 123})
|
||||||
|
[%Pipeline{}, ...]
|
||||||
|
|
||||||
|
iex> backlink("[other-pipeline]", %User{id: 123})
|
||||||
|
[%Pipeline{description: "[other-pipeline]"}, ...]
|
||||||
|
|
||||||
|
"""
|
||||||
|
@spec backlink(String.t(), User.t()) :: [Pipeline.t()]
|
||||||
|
def backlink(link, %{id: user_id}) when user_id |> is_binary() do
|
||||||
|
link = link |> String.replace("[", "\\[") |> String.replace("]", "\\]")
|
||||||
|
link_regex = "(^|[^\[])#{link}($|[^\]])"
|
||||||
|
|
||||||
|
Repo.all(
|
||||||
|
from p in Pipeline,
|
||||||
|
left_join: s in assoc(p, :steps),
|
||||||
|
where:
|
||||||
|
fragment("? ~ ?", p.description, ^link_regex) or
|
||||||
|
fragment("? ~ ?", s.content, ^link_regex),
|
||||||
|
distinct: true,
|
||||||
|
order_by: p.slug
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def backlink(link, _invalid_user) do
|
||||||
|
link = link |> String.replace("[", "\\[") |> String.replace("]", "\\]")
|
||||||
|
link_regex = "(^|[^\[])#{link}($|[^\]])"
|
||||||
|
|
||||||
|
Repo.all(
|
||||||
|
from p in Pipeline,
|
||||||
|
left_join: s in assoc(p, :steps),
|
||||||
|
where:
|
||||||
|
fragment("? ~ ?", p.description, ^link_regex) or
|
||||||
|
fragment("? ~ ?", s.content, ^link_regex),
|
||||||
|
where: p.visibility == :public,
|
||||||
|
distinct: true,
|
||||||
|
order_by: p.slug
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Creates a pipeline.
|
Creates a pipeline.
|
||||||
|
|
||||||
@ -193,23 +230,16 @@ defmodule Memex.Pipelines do
|
|||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
iex> delete_pipeline(%Pipeline{user_id: 123}, %User{id: 123})
|
|
||||||
{:ok, %Pipeline{}}
|
|
||||||
|
|
||||||
iex> delete_pipeline(%Pipeline{}, %User{role: :admin})
|
|
||||||
{:ok, %Pipeline{}}
|
|
||||||
|
|
||||||
iex> delete_pipeline(%Pipeline{}, %User{id: 123})
|
iex> delete_pipeline(%Pipeline{}, %User{id: 123})
|
||||||
|
{:ok, %Pipeline{}}
|
||||||
|
|
||||||
|
iex> delete_pipeline(%Pipeline{}, nil)
|
||||||
{:error, %Ecto.Changeset{}}
|
{:error, %Ecto.Changeset{}}
|
||||||
|
|
||||||
"""
|
"""
|
||||||
@spec delete_pipeline(Pipeline.t(), User.t()) ::
|
@spec delete_pipeline(Pipeline.t(), User.t()) ::
|
||||||
{:ok, Pipeline.t()} | {:error, Pipeline.changeset()}
|
{:ok, Pipeline.t()} | {:error, Pipeline.changeset()}
|
||||||
def delete_pipeline(%Pipeline{user_id: user_id} = pipeline, %{id: user_id}) do
|
def delete_pipeline(%Pipeline{} = pipeline, %{id: user_id}) when user_id |> is_binary() do
|
||||||
pipeline |> Repo.delete()
|
|
||||||
end
|
|
||||||
|
|
||||||
def delete_pipeline(%Pipeline{} = pipeline, %{role: :admin}) do
|
|
||||||
pipeline |> Repo.delete()
|
pipeline |> Repo.delete()
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -230,13 +260,4 @@ defmodule Memex.Pipelines do
|
|||||||
def change_pipeline(%Pipeline{} = pipeline, attrs \\ %{}, user) do
|
def change_pipeline(%Pipeline{} = pipeline, attrs \\ %{}, user) do
|
||||||
pipeline |> Pipeline.update_changeset(attrs, user)
|
pipeline |> Pipeline.update_changeset(attrs, user)
|
||||||
end
|
end
|
||||||
|
|
||||||
@spec is_owner_or_admin?(Pipeline.t(), User.t()) :: boolean()
|
|
||||||
def is_owner_or_admin?(%{user_id: user_id}, %{id: user_id}), do: true
|
|
||||||
def is_owner_or_admin?(_context, %{role: :admin}), do: true
|
|
||||||
def is_owner_or_admin?(_context, _other_user), do: false
|
|
||||||
|
|
||||||
@spec is_owner?(Pipeline.t(), User.t()) :: boolean()
|
|
||||||
def is_owner?(%{user_id: user_id}, %{id: user_id}), do: true
|
|
||||||
def is_owner?(_context, _other_user), do: false
|
|
||||||
end
|
end
|
||||||
|
@ -3,11 +3,12 @@ defmodule Memex.Pipelines.Pipeline do
|
|||||||
Represents a chain of considerations to take to accomplish a task
|
Represents a chain of considerations to take to accomplish a task
|
||||||
"""
|
"""
|
||||||
use Ecto.Schema
|
use Ecto.Schema
|
||||||
|
use Gettext, backend: MemexWeb.Gettext
|
||||||
import Ecto.Changeset
|
import Ecto.Changeset
|
||||||
import MemexWeb.Gettext
|
|
||||||
alias Ecto.{Changeset, UUID}
|
alias Ecto.{Changeset, UUID}
|
||||||
alias Memex.{Accounts.User, Pipelines.Steps.Step, Repo}
|
alias Memex.{Accounts.User, Pipelines.Steps.Step, Repo}
|
||||||
|
|
||||||
|
@derive {Phoenix.Param, key: :slug}
|
||||||
@derive {Jason.Encoder,
|
@derive {Jason.Encoder,
|
||||||
only: [
|
only: [
|
||||||
:slug,
|
:slug,
|
||||||
@ -64,7 +65,8 @@ defmodule Memex.Pipelines.Pipeline do
|
|||||||
end
|
end
|
||||||
|
|
||||||
@spec update_changeset(t(), attrs :: map(), User.t()) :: changeset()
|
@spec update_changeset(t(), attrs :: map(), User.t()) :: changeset()
|
||||||
def update_changeset(%{user_id: user_id} = pipeline, attrs, %User{id: user_id}) do
|
def update_changeset(%__MODULE__{} = pipeline, attrs, %User{id: user_id})
|
||||||
|
when user_id |> is_binary() do
|
||||||
pipeline
|
pipeline
|
||||||
|> cast(attrs, [:slug, :description, :tags, :visibility])
|
|> cast(attrs, [:slug, :description, :tags, :visibility])
|
||||||
|> cast_tags_string(attrs)
|
|> cast_tags_string(attrs)
|
||||||
@ -80,11 +82,11 @@ defmodule Memex.Pipelines.Pipeline do
|
|||||||
changeset
|
changeset
|
||||||
|> put_change(:tags_string, changeset |> get_field(:tags) |> get_tags_string())
|
|> put_change(:tags_string, changeset |> get_field(:tags) |> get_tags_string())
|
||||||
|> cast(attrs, [:tags_string])
|
|> cast(attrs, [:tags_string])
|
||||||
|> validate_format(:tags_string, ~r/^[\p{L}\p{N}\-\,]+$/,
|
|> validate_format(:tags_string, ~r/^[\p{L}\p{N}\-\, ]+$/,
|
||||||
message:
|
message:
|
||||||
dgettext(
|
dgettext(
|
||||||
"errors",
|
"errors",
|
||||||
"invalid format: only numbers, letters and hyphen are accepted. tags must be comma-delimited"
|
"invalid format: only numbers, letters and hyphen are accepted. tags must be comma or space delimited"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|> cast_tags()
|
|> cast_tags()
|
||||||
@ -99,9 +101,9 @@ defmodule Memex.Pipelines.Pipeline do
|
|||||||
|
|
||||||
defp process_tags(tags_string) when tags_string |> is_binary() do
|
defp process_tags(tags_string) when tags_string |> is_binary() do
|
||||||
tags_string
|
tags_string
|
||||||
|> String.split(",", trim: true)
|
|> String.split([",", " "], trim: true)
|
||||||
|> Enum.map(fn str -> str |> String.trim() end)
|
|> Enum.map(fn str -> str |> String.trim() end)
|
||||||
|> Enum.reject(fn str -> str |> is_nil() end)
|
|> Enum.reject(fn str -> str in [nil, ""] end)
|
||||||
|> Enum.sort()
|
|> Enum.sort()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -44,35 +44,32 @@ defmodule Memex.Pipelines.Steps.Step do
|
|||||||
@doc false
|
@doc false
|
||||||
@spec create_changeset(attrs :: map(), position :: non_neg_integer(), Pipeline.t(), User.t()) ::
|
@spec create_changeset(attrs :: map(), position :: non_neg_integer(), Pipeline.t(), User.t()) ::
|
||||||
changeset()
|
changeset()
|
||||||
def create_changeset(attrs, position, %Pipeline{id: pipeline_id, user_id: user_id}, %User{
|
def create_changeset(
|
||||||
id: user_id
|
attrs,
|
||||||
}) do
|
position,
|
||||||
|
%Pipeline{id: pipeline_id, user_id: user_id},
|
||||||
|
%User{id: user_id}
|
||||||
|
) do
|
||||||
%__MODULE__{}
|
%__MODULE__{}
|
||||||
|> cast(attrs, [:title, :content])
|
|> cast(attrs, [:title, :content])
|
||||||
|> change(pipeline_id: pipeline_id, user_id: user_id, position: position)
|
|> change(pipeline_id: pipeline_id, user_id: user_id, position: position)
|
||||||
|> validate_required([:title, :content, :user_id, :position])
|
|> validate_required([:title, :user_id, :position])
|
||||||
end
|
end
|
||||||
|
|
||||||
@spec update_changeset(t(), attrs :: map(), User.t()) ::
|
@spec update_changeset(t(), attrs :: map(), User.t()) ::
|
||||||
changeset()
|
changeset()
|
||||||
def update_changeset(
|
def update_changeset(%__MODULE__{} = step, attrs, %User{id: user_id})
|
||||||
%{user_id: user_id} = step,
|
when user_id |> is_binary() do
|
||||||
attrs,
|
|
||||||
%User{id: user_id}
|
|
||||||
) do
|
|
||||||
step
|
step
|
||||||
|> cast(attrs, [:title, :content])
|
|> cast(attrs, [:title, :content])
|
||||||
|> validate_required([:title, :content, :user_id, :position])
|
|> validate_required([:title, :user_id, :position])
|
||||||
end
|
end
|
||||||
|
|
||||||
@spec position_changeset(t(), position :: non_neg_integer(), User.t()) :: changeset()
|
@spec position_changeset(t(), position :: non_neg_integer(), User.t()) :: changeset()
|
||||||
def position_changeset(
|
def position_changeset(%__MODULE__{} = step, position, %User{id: user_id})
|
||||||
%{user_id: user_id} = step,
|
when user_id |> is_binary() do
|
||||||
position,
|
|
||||||
%User{id: user_id}
|
|
||||||
) do
|
|
||||||
step
|
step
|
||||||
|> change(position: position)
|
|> change(position: position)
|
||||||
|> validate_required([:title, :content, :user_id, :position])
|
|> validate_required([:title, :user_id, :position])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -21,11 +21,10 @@ defmodule Memex.Pipelines.Steps do
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
@spec list_steps(Pipeline.t(), User.t()) :: [Step.t()]
|
@spec list_steps(Pipeline.t(), User.t()) :: [Step.t()]
|
||||||
def list_steps(%{id: pipeline_id}, %{id: user_id}) do
|
def list_steps(%{id: pipeline_id}, %{id: user_id}) when user_id |> is_binary() do
|
||||||
Repo.all(
|
Repo.all(
|
||||||
from s in Step,
|
from s in Step,
|
||||||
where: s.pipeline_id == ^pipeline_id,
|
where: s.pipeline_id == ^pipeline_id,
|
||||||
where: s.user_id == ^user_id,
|
|
||||||
order_by: s.position
|
order_by: s.position
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
@ -62,8 +61,8 @@ defmodule Memex.Pipelines.Steps do
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
@spec get_step!(Step.id(), User.t()) :: Step.t()
|
@spec get_step!(Step.id(), User.t()) :: Step.t()
|
||||||
def get_step!(id, %{id: user_id}) do
|
def get_step!(id, %{id: user_id}) when user_id |> is_binary() do
|
||||||
Repo.one!(from n in Step, where: n.id == ^id, where: n.user_id == ^user_id)
|
Repo.one!(from n in Step, where: n.id == ^id)
|
||||||
end
|
end
|
||||||
|
|
||||||
def get_step!(id, _invalid_user) do
|
def get_step!(id, _invalid_user) do
|
||||||
@ -119,22 +118,15 @@ defmodule Memex.Pipelines.Steps do
|
|||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
iex> delete_step(%Step{user_id: 123}, %User{id: 123})
|
|
||||||
{:ok, %Step{}}
|
|
||||||
|
|
||||||
iex> delete_step(%Step{}, %User{role: :admin})
|
|
||||||
{:ok, %Step{}}
|
|
||||||
|
|
||||||
iex> delete_step(%Step{}, %User{id: 123})
|
iex> delete_step(%Step{}, %User{id: 123})
|
||||||
|
{:ok, %Step{}}
|
||||||
|
|
||||||
|
iex> delete_step(%Step{}, nil)
|
||||||
{:error, %Ecto.Changeset{}}
|
{:error, %Ecto.Changeset{}}
|
||||||
|
|
||||||
"""
|
"""
|
||||||
@spec delete_step(Step.t(), User.t()) :: {:ok, Step.t()} | {:error, Step.changeset()}
|
@spec delete_step(Step.t(), User.t()) :: {:ok, Step.t()} | {:error, Step.changeset()}
|
||||||
def delete_step(%Step{user_id: user_id} = step, %{id: user_id}) do
|
def delete_step(%Step{} = step, %{id: user_id}) when user_id |> is_binary() do
|
||||||
delete_step(step)
|
|
||||||
end
|
|
||||||
|
|
||||||
def delete_step(%Step{} = step, %{role: :admin}) do
|
|
||||||
delete_step(step)
|
delete_step(step)
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -181,10 +173,11 @@ defmodule Memex.Pipelines.Steps do
|
|||||||
def reorder_step(%Step{position: 0} = step, :up, _user), do: {:error, step}
|
def reorder_step(%Step{position: 0} = step, :up, _user), do: {:error, step}
|
||||||
|
|
||||||
def reorder_step(
|
def reorder_step(
|
||||||
%Step{position: position, pipeline_id: pipeline_id, user_id: user_id} = step,
|
%Step{position: position, pipeline_id: pipeline_id} = step,
|
||||||
:up,
|
:up,
|
||||||
%{id: user_id} = user
|
%{id: user_id} = user
|
||||||
) do
|
)
|
||||||
|
when user_id |> is_binary() do
|
||||||
Multi.new()
|
Multi.new()
|
||||||
|> Multi.update_all(
|
|> Multi.update_all(
|
||||||
:reorder_steps,
|
:reorder_steps,
|
||||||
@ -207,10 +200,11 @@ defmodule Memex.Pipelines.Steps do
|
|||||||
end
|
end
|
||||||
|
|
||||||
def reorder_step(
|
def reorder_step(
|
||||||
%Step{pipeline_id: pipeline_id, position: position, user_id: user_id} = step,
|
%Step{pipeline_id: pipeline_id, position: position} = step,
|
||||||
:down,
|
:down,
|
||||||
%{id: user_id} = user
|
%{id: user_id} = user
|
||||||
) do
|
)
|
||||||
|
when user_id |> is_binary() do
|
||||||
Multi.new()
|
Multi.new()
|
||||||
|> Multi.one(
|
|> Multi.one(
|
||||||
:step_count,
|
:step_count,
|
||||||
|
109
lib/memex_web.ex
109
lib/memex_web.ex
@ -1,54 +1,62 @@
|
|||||||
defmodule MemexWeb do
|
defmodule MemexWeb do
|
||||||
@moduledoc """
|
@moduledoc """
|
||||||
The entrypoint for defining your web interface, such
|
The entrypoint for defining your web interface, such
|
||||||
as controllers, views, channels and so on.
|
as controllers, components, channels, and so on.
|
||||||
|
|
||||||
This can be used in your application as:
|
This can be used in your application as:
|
||||||
|
|
||||||
use MemexWeb, :controller
|
use MemexWeb, :controller
|
||||||
use MemexWeb, :view
|
use MemexWeb, :html
|
||||||
|
|
||||||
The definitions below will be executed for every view,
|
The definitions below will be executed for every controller,
|
||||||
controller, etc, so keep them short and clean, focused
|
component, etc, so keep them short and clean, focused
|
||||||
on imports, uses and aliases.
|
on imports, uses and aliases.
|
||||||
|
|
||||||
Do NOT define functions inside the quoted expressions
|
Do NOT define functions inside the quoted expressions
|
||||||
below. Instead, define any helper function in modules
|
below. Instead, define additional modules and import
|
||||||
and import those modules here.
|
those modules here.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def controller do
|
def static_paths, do: ~w(css js fonts images favicon.ico robots.txt)
|
||||||
quote do
|
|
||||||
use Phoenix.Controller, namespace: MemexWeb
|
|
||||||
|
|
||||||
|
def router do
|
||||||
|
quote do
|
||||||
|
use Phoenix.Router, helpers: false
|
||||||
|
|
||||||
|
# Import common connection and controller functions to use in pipelines
|
||||||
import Plug.Conn
|
import Plug.Conn
|
||||||
import MemexWeb.Gettext
|
import Phoenix.Controller
|
||||||
alias MemexWeb.Endpoint
|
import Phoenix.LiveView.Router
|
||||||
alias MemexWeb.Router.Helpers, as: Routes
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def view do
|
def channel do
|
||||||
quote do
|
quote do
|
||||||
use Phoenix.View,
|
use Phoenix.Channel
|
||||||
root: "lib/memex_web/templates",
|
end
|
||||||
namespace: MemexWeb
|
end
|
||||||
|
|
||||||
# Import convenience functions from controllers
|
def controller do
|
||||||
import Phoenix.Controller,
|
quote do
|
||||||
only: [get_flash: 1, get_flash: 2, view_module: 1, view_template: 1]
|
use Phoenix.Controller,
|
||||||
|
formats: [:html, :json],
|
||||||
|
layouts: [html: MemexWeb.Layouts]
|
||||||
|
|
||||||
# Include shared imports and aliases for views
|
use Gettext, backend: MemexWeb.Gettext
|
||||||
unquote(view_helpers())
|
|
||||||
|
import MemexWeb.ControllerHelpers
|
||||||
|
import Plug.Conn
|
||||||
|
|
||||||
|
unquote(verified_routes())
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def live_view do
|
def live_view do
|
||||||
quote do
|
quote do
|
||||||
use Phoenix.LiveView, layout: {MemexWeb.LayoutView, :live}
|
use Phoenix.LiveView,
|
||||||
|
layout: {MemexWeb.Layouts, :app}
|
||||||
|
|
||||||
on_mount MemexWeb.InitAssigns
|
unquote(html_helpers())
|
||||||
unquote(view_helpers())
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -56,50 +64,45 @@ defmodule MemexWeb do
|
|||||||
quote do
|
quote do
|
||||||
use Phoenix.LiveComponent
|
use Phoenix.LiveComponent
|
||||||
|
|
||||||
unquote(view_helpers())
|
unquote(html_helpers())
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def component do
|
def html do
|
||||||
quote do
|
quote do
|
||||||
|
# credo:disable-for-next-line Credo.Check.Consistency.MultiAliasImportRequireUse
|
||||||
use Phoenix.Component
|
use Phoenix.Component
|
||||||
|
|
||||||
unquote(view_helpers())
|
# Import convenience functions from controllers
|
||||||
|
import Phoenix.Controller,
|
||||||
|
only: [get_csrf_token: 0, view_module: 1, view_template: 1]
|
||||||
|
|
||||||
|
# Include general helpers for rendering HTML
|
||||||
|
unquote(html_helpers())
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def router do
|
defp html_helpers do
|
||||||
quote do
|
quote do
|
||||||
use Phoenix.Router
|
use PhoenixHTMLHelpers
|
||||||
|
use Gettext, backend: MemexWeb.Gettext
|
||||||
|
import Phoenix.{Component, HTML, HTML.Form}
|
||||||
|
import MemexWeb.{ErrorHelpers, CoreComponents, HTMLHelpers}
|
||||||
|
|
||||||
import Phoenix.{Controller, LiveView.Router}
|
# Shortcut for generating JS commands
|
||||||
# credo:disable-for-next-line Credo.Check.Consistency.MultiAliasImportRequireUse
|
alias Phoenix.LiveView.JS
|
||||||
import Plug.Conn
|
|
||||||
|
# Routes generation with the ~p sigil
|
||||||
|
unquote(verified_routes())
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def channel do
|
def verified_routes do
|
||||||
quote do
|
quote do
|
||||||
use Phoenix.Channel
|
use Phoenix.VerifiedRoutes,
|
||||||
# credo:disable-for-next-line Credo.Check.Consistency.MultiAliasImportRequireUse
|
endpoint: MemexWeb.Endpoint,
|
||||||
import MemexWeb.Gettext
|
router: MemexWeb.Router,
|
||||||
end
|
statics: MemexWeb.static_paths()
|
||||||
end
|
|
||||||
|
|
||||||
defp view_helpers do
|
|
||||||
quote do
|
|
||||||
# Use all HTML functionality (forms, tags, etc)
|
|
||||||
# credo:disable-for-next-line Credo.Check.Consistency.MultiAliasImportRequireUse
|
|
||||||
use Phoenix.HTML
|
|
||||||
|
|
||||||
# Import LiveView and .heex helpers (live_render, link, <.form>, etc)
|
|
||||||
# Import basic rendering functionality (render, render_layout, etc)
|
|
||||||
import Phoenix.{Component, View}
|
|
||||||
import MemexWeb.{ErrorHelpers, Gettext, CoreComponents, ViewHelpers}
|
|
||||||
|
|
||||||
# credo:disable-for-next-line Credo.Check.Consistency.MultiAliasImportRequireUse
|
|
||||||
alias MemexWeb.Endpoint
|
|
||||||
alias MemexWeb.Router.Helpers, as: Routes
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -37,7 +37,7 @@ defmodule MemexWeb.Components.ContextsTableComponent do
|
|||||||
} = socket
|
} = socket
|
||||||
) do
|
) do
|
||||||
columns =
|
columns =
|
||||||
if actions == [] or current_user |> is_nil() do
|
if actions == [] or !current_user do
|
||||||
[]
|
[]
|
||||||
else
|
else
|
||||||
[%{label: gettext("actions"), key: :actions, sortable: false}]
|
[%{label: gettext("actions"), key: :actions, sortable: false}]
|
||||||
@ -88,11 +88,9 @@ defmodule MemexWeb.Components.ContextsTableComponent do
|
|||||||
|
|
||||||
@spec get_value_for_key(atom(), Context.t(), additional_data :: map()) ::
|
@spec get_value_for_key(atom(), Context.t(), additional_data :: map()) ::
|
||||||
any() | {any(), Rendered.t()}
|
any() | {any(), Rendered.t()}
|
||||||
defp get_value_for_key(:slug, %{slug: slug}, _additional_data) do
|
defp get_value_for_key(:slug, %{slug: slug} = assigns, _additional_data) do
|
||||||
assigns = %{slug: slug}
|
|
||||||
|
|
||||||
slug_block = ~H"""
|
slug_block = ~H"""
|
||||||
<.link navigate={Routes.context_show_path(Endpoint, :show, @slug)} class="link">
|
<.link navigate={~p"/context/#{@slug}"} class="link">
|
||||||
<%= @slug %>
|
<%= @slug %>
|
||||||
</.link>
|
</.link>
|
||||||
"""
|
"""
|
||||||
@ -100,16 +98,10 @@ defmodule MemexWeb.Components.ContextsTableComponent do
|
|||||||
{slug, slug_block}
|
{slug, slug_block}
|
||||||
end
|
end
|
||||||
|
|
||||||
defp get_value_for_key(:tags, %{tags: tags}, _additional_data) do
|
defp get_value_for_key(:tags, assigns, _additional_data) do
|
||||||
assigns = %{tags: tags}
|
|
||||||
|
|
||||||
~H"""
|
~H"""
|
||||||
<div class="flex flex-wrap justify-center space-x-1">
|
<div class="flex flex-wrap justify-center space-x-1">
|
||||||
<.link
|
<.link :for={tag <- @tags} patch={~p"/contexts/#{tag}"} class="link">
|
||||||
:for={tag <- @tags}
|
|
||||||
patch={Routes.context_index_path(Endpoint, :search, tag)}
|
|
||||||
class="link"
|
|
||||||
>
|
|
||||||
<%= tag %>
|
<%= tag %>
|
||||||
</.link>
|
</.link>
|
||||||
</div>
|
</div>
|
||||||
|
@ -2,14 +2,15 @@ defmodule MemexWeb.CoreComponents do
|
|||||||
@moduledoc """
|
@moduledoc """
|
||||||
Provides core UI components.
|
Provides core UI components.
|
||||||
"""
|
"""
|
||||||
|
use PhoenixHTMLHelpers
|
||||||
use Phoenix.Component
|
use Phoenix.Component
|
||||||
import MemexWeb.{Gettext, ViewHelpers}
|
use MemexWeb, :verified_routes
|
||||||
|
use Gettext, backend: MemexWeb.Gettext
|
||||||
|
import MemexWeb.HTMLHelpers
|
||||||
alias Memex.{Accounts, Accounts.Invite, Accounts.User}
|
alias Memex.{Accounts, Accounts.Invite, Accounts.User}
|
||||||
alias Memex.Contexts.Context
|
alias Memex.Contexts.Context
|
||||||
alias Memex.Notes.Note
|
alias Memex.Notes.Note
|
||||||
alias Memex.Pipelines.Steps.Step
|
alias Memex.Pipelines.{Pipeline, Steps.Step}
|
||||||
alias MemexWeb.{Endpoint, HomeLive}
|
|
||||||
alias MemexWeb.Router.Helpers, as: Routes
|
|
||||||
alias Phoenix.HTML
|
alias Phoenix.HTML
|
||||||
alias Phoenix.LiveView.JS
|
alias Phoenix.LiveView.JS
|
||||||
|
|
||||||
@ -31,13 +32,13 @@ defmodule MemexWeb.CoreComponents do
|
|||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
<.modal return_to={Routes.<%= schema.singular %>_index_path(Endpoint, :index)}>
|
<.modal return_to={~p"/\#{<%= schema.plural %>}"}>
|
||||||
<.live_component
|
<.live_component
|
||||||
module={<%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>Live.FormComponent}
|
module={<%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>Live.FormComponent}
|
||||||
id={@<%= schema.singular %>.id || :new}
|
id={@<%= schema.singular %>.id || :new}
|
||||||
title={@page_title}
|
title={@page_title}
|
||||||
action={@live_action}
|
action={@live_action}
|
||||||
return_to={Routes.<%= schema.singular %>_index_path(Endpoint, :index)}
|
return_to={~p"/\#{<%= schema.singular %>}"}
|
||||||
<%= schema.singular %>: @<%= schema.singular %>
|
<%= schema.singular %>: @<%= schema.singular %>
|
||||||
/>
|
/>
|
||||||
</.modal>
|
</.modal>
|
||||||
@ -131,53 +132,128 @@ defmodule MemexWeb.CoreComponents do
|
|||||||
|
|
||||||
def step_content(assigns)
|
def step_content(assigns)
|
||||||
|
|
||||||
defp add_links_to_content(content, data_qa_prefix) do
|
attr :pipeline, Pipeline, required: true
|
||||||
# replace links
|
|
||||||
|
|
||||||
# link regex from
|
def pipeline_content(assigns)
|
||||||
# https://stackoverflow.com/questions/3809401/what-is-a-good-regular-expression-to-match-a-url
|
|
||||||
# and modified with additional schemes from
|
|
||||||
# https://www.iana.org/assignments/uri-schemes/uri-schemes.xhtml
|
|
||||||
|
|
||||||
content =
|
defp display_links(record) do
|
||||||
Regex.replace(
|
record
|
||||||
~r<((file|git|https?|ipfs|ipns|irc|jabber|magnet|mailto|mumble|tel|udp|xmpp):\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*))>,
|
|> get_content()
|
||||||
content,
|
|> Phoenix.HTML.html_escape()
|
||||||
fn _whole_match, link ->
|
|> Phoenix.HTML.safe_to_string()
|
||||||
link =
|
|> replace_hyperlinks(record)
|
||||||
HTML.Link.link(
|
|> replace_triple_links(record)
|
||||||
link,
|
|> replace_double_links(record)
|
||||||
to: link,
|
|> replace_single_links(record)
|
||||||
class: "link inline",
|
|> HTML.raw()
|
||||||
target: "_blank",
|
end
|
||||||
rel: "noopener noreferrer"
|
|
||||||
)
|
|
||||||
|> HTML.Safe.to_iodata()
|
|
||||||
|> IO.iodata_to_binary()
|
|
||||||
|
|
||||||
"</p>#{link}<p class=\"inline\">"
|
defp get_content(%{content: content}), do: content |> get_text()
|
||||||
end
|
defp get_content(%{description: description}), do: description |> get_text()
|
||||||
)
|
defp get_content(_fallthrough), do: nil |> get_text()
|
||||||
|
|
||||||
content =
|
defp get_text(string) when is_binary(string), do: string
|
||||||
Regex.replace(
|
defp get_text(_fallthrough), do: ""
|
||||||
~r/\[\[([\p{L}\p{N}\-]+)\]\]/,
|
|
||||||
content,
|
|
||||||
fn _whole_match, slug ->
|
|
||||||
link =
|
|
||||||
HTML.Link.link(
|
|
||||||
"[[#{slug}]]",
|
|
||||||
to: Routes.note_show_path(Endpoint, :show, slug),
|
|
||||||
class: "link inline",
|
|
||||||
data: [qa: "#{data_qa_prefix}-#{slug}"]
|
|
||||||
)
|
|
||||||
|> HTML.Safe.to_iodata()
|
|
||||||
|> IO.iodata_to_binary()
|
|
||||||
|
|
||||||
"</p>#{link}<p class=\"inline\">"
|
# replaces hyperlinks like https://bubbletea.dev
|
||||||
end
|
#
|
||||||
)
|
# link regex from
|
||||||
|
# https://stackoverflow.com/questions/3809401/what-is-a-good-regular-expression-to-match-a-url
|
||||||
|
# and modified with additional schemes from
|
||||||
|
# https://www.iana.org/assignments/uri-schemes/uri-schemes.xhtml
|
||||||
|
defp replace_hyperlinks(content, _record) do
|
||||||
|
Regex.replace(
|
||||||
|
~r<((file|git|https?|ipfs|ipns|irc|jabber|magnet|mailto|mumble|tel|udp|xmpp):\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*))>,
|
||||||
|
content,
|
||||||
|
fn _whole_match, link ->
|
||||||
|
link =
|
||||||
|
link(
|
||||||
|
link,
|
||||||
|
to: link,
|
||||||
|
class: "link inline break-words",
|
||||||
|
target: "_blank",
|
||||||
|
rel: "noopener noreferrer"
|
||||||
|
)
|
||||||
|
|> HTML.Safe.to_iodata()
|
||||||
|
|> IO.iodata_to_binary()
|
||||||
|
|
||||||
content |> HTML.raw()
|
"</p>#{link}<p class=\"inline break-words\">"
|
||||||
|
end
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
# replaces triple links like [[[slug-title]]]
|
||||||
|
defp replace_triple_links(content, _record) do
|
||||||
|
Regex.replace(
|
||||||
|
~r/(^|[^\[])\[\[\[([\p{L}\p{N}\-]+)\]\]\]($|[^\]])/,
|
||||||
|
content,
|
||||||
|
fn _whole_match, prefix, slug, suffix ->
|
||||||
|
link =
|
||||||
|
link(
|
||||||
|
"[[[#{slug}]]]",
|
||||||
|
to: ~p"/note/#{slug}",
|
||||||
|
class: "link inline break-words"
|
||||||
|
)
|
||||||
|
|> HTML.Safe.to_iodata()
|
||||||
|
|> IO.iodata_to_binary()
|
||||||
|
|
||||||
|
"#{prefix}</p>#{link}<p class=\"inline break-words\">#{suffix}"
|
||||||
|
end
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
# replaces double links like [[slug-title]]
|
||||||
|
defp replace_double_links(content, record) do
|
||||||
|
Regex.replace(
|
||||||
|
~r/(^|[^\[])\[\[([\p{L}\p{N}\-]+)\]\]($|[^\]])/,
|
||||||
|
content,
|
||||||
|
fn _whole_match, prefix, slug, suffix ->
|
||||||
|
target =
|
||||||
|
case record do
|
||||||
|
%Pipeline{} -> ~p"/context/#{slug}"
|
||||||
|
%Step{} -> ~p"/context/#{slug}"
|
||||||
|
_context -> ~p"/note/#{slug}"
|
||||||
|
end
|
||||||
|
|
||||||
|
link =
|
||||||
|
link(
|
||||||
|
"[[#{slug}]]",
|
||||||
|
to: target,
|
||||||
|
class: "link inline break-words"
|
||||||
|
)
|
||||||
|
|> HTML.Safe.to_iodata()
|
||||||
|
|> IO.iodata_to_binary()
|
||||||
|
|
||||||
|
"#{prefix}</p>#{link}<p class=\"inline break-words\">#{suffix}"
|
||||||
|
end
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
# replaces single links like [slug-title]
|
||||||
|
defp replace_single_links(content, record) do
|
||||||
|
Regex.replace(
|
||||||
|
~r/(^|[^\[])\[([\p{L}\p{N}\-]+)\]($|[^\]])/,
|
||||||
|
content,
|
||||||
|
fn _whole_match, prefix, slug, suffix ->
|
||||||
|
target =
|
||||||
|
case record do
|
||||||
|
%Pipeline{} -> ~p"/pipeline/#{slug}"
|
||||||
|
%Step{} -> ~p"/pipeline/#{slug}"
|
||||||
|
%Context{} -> ~p"/context/#{slug}"
|
||||||
|
_note -> ~p"/note/#{slug}"
|
||||||
|
end
|
||||||
|
|
||||||
|
link =
|
||||||
|
link(
|
||||||
|
"[#{slug}]",
|
||||||
|
to: target,
|
||||||
|
class: "link inline break-words"
|
||||||
|
)
|
||||||
|
|> HTML.Safe.to_iodata()
|
||||||
|
|> IO.iodata_to_binary()
|
||||||
|
|
||||||
|
"#{prefix}</p>#{link}<p class=\"inline break-words\">#{suffix}"
|
||||||
|
end
|
||||||
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -1,8 +1,11 @@
|
|||||||
<div
|
<div
|
||||||
|
:if={@context.content}
|
||||||
id={"show-context-content-#{@context.id}"}
|
id={"show-context-content-#{@context.id}"}
|
||||||
class="input input-primary h-128 min-h-128 inline-block whitespace-pre-wrap overflow-x-hidden overflow-y-auto"
|
class="inline-block overflow-y-auto overflow-x-hidden whitespace-pre-wrap resize-y input input-primary h-128 min-h-128"
|
||||||
phx-hook="MaintainAttrs"
|
|
||||||
phx-update="ignore"
|
phx-update="ignore"
|
||||||
readonly
|
readonly
|
||||||
phx-no-format
|
phx-no-format
|
||||||
><p class="inline"><%= add_links_to_content(@context.content, "context-note") %></p></div>
|
><p class="inline"><%= display_links(@context) %></p></div>
|
||||||
|
<div :if={!@context.content} class="text-sm italic text-center text-gray-600">
|
||||||
|
<%= gettext("(This context is empty)") %>
|
||||||
|
</div>
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
<div class="px-8 py-4 flex flex-col justify-center items-center space-y-4
|
<div class="flex flex-col justify-center items-center px-8 py-4 space-y-4 rounded-lg border border-gray-400 shadow-lg transition-all duration-300 ease-in-out bg-primary-900 hover:shadow-md">
|
||||||
bg-primary-900
|
<h1 class="text-xl title">
|
||||||
border border-gray-400 rounded-lg shadow-lg hover:shadow-md
|
|
||||||
transition-all duration-300 ease-in-out">
|
|
||||||
<h1 class="title text-xl">
|
|
||||||
<%= @invite.name %>
|
<%= @invite.name %>
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<%= if @invite.disabled_at |> is_nil() do %>
|
<%= if @invite.disabled_at do %>
|
||||||
|
<h2 class="title text-md">
|
||||||
|
<%= gettext("invite disabled") %>
|
||||||
|
</h2>
|
||||||
|
<% else %>
|
||||||
<h2 class="title text-md">
|
<h2 class="title text-md">
|
||||||
<%= if @invite.uses_left do %>
|
<%= if @invite.uses_left do %>
|
||||||
<%= gettext(
|
<%= gettext(
|
||||||
@ -17,14 +18,10 @@
|
|||||||
<%= gettext("uses left: unlimited") %>
|
<%= gettext("uses left: unlimited") %>
|
||||||
<% end %>
|
<% end %>
|
||||||
</h2>
|
</h2>
|
||||||
<% else %>
|
|
||||||
<h2 class="title text-md">
|
|
||||||
<%= gettext("invite disabled") %>
|
|
||||||
</h2>
|
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<.qr_code
|
<.qr_code
|
||||||
content={Routes.user_registration_url(Endpoint, :new, invite: @invite.token)}
|
content={url(MemexWeb.Endpoint, ~p"/users/register?invite=#{@invite.token}")}
|
||||||
filename={@invite.name}
|
filename={@invite.name}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@ -35,14 +32,13 @@
|
|||||||
<div class="flex flex-row flex-wrap justify-center items-center">
|
<div class="flex flex-row flex-wrap justify-center items-center">
|
||||||
<code
|
<code
|
||||||
id={"code-#{@invite.id}"}
|
id={"code-#{@invite.id}"}
|
||||||
class="mx-2 my-1 text-xs px-4 py-2 rounded-lg text-center break-all
|
class="px-4 py-2 mx-2 my-1 text-xs text-center break-all rounded-lg text-primary-400 bg-primary-800"
|
||||||
text-primary-400 bg-primary-800"
|
|
||||||
phx-no-format
|
phx-no-format
|
||||||
><%= Routes.user_registration_url(Endpoint, :new, invite: @invite.token) %></code>
|
><%= url(MemexWeb.Endpoint, ~p"/users/register?invite=#{@invite.token}") %></code>
|
||||||
<%= if @code_actions, do: render_slot(@code_actions) %>
|
<%= if @code_actions, do: render_slot(@code_actions) %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div :if={@inner_block} class="flex space-x-4 justify-center items-center">
|
<div :if={@inner_block} class="flex justify-center items-center space-x-4">
|
||||||
<%= render_slot(@inner_block) %>
|
<%= render_slot(@inner_block) %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,8 +1,11 @@
|
|||||||
<div
|
<div
|
||||||
|
:if={@note.content}
|
||||||
id={"show-note-content-#{@note.id}"}
|
id={"show-note-content-#{@note.id}"}
|
||||||
class="input input-primary h-128 min-h-128 inline-block whitespace-pre-wrap overflow-x-hidden overflow-y-auto"
|
class="inline-block overflow-y-auto overflow-x-hidden whitespace-pre-wrap resize-y input input-primary h-128 min-h-128"
|
||||||
phx-hook="MaintainAttrs"
|
|
||||||
phx-update="ignore"
|
phx-update="ignore"
|
||||||
readonly
|
readonly
|
||||||
phx-no-format
|
phx-no-format
|
||||||
><p class="inline"><%= add_links_to_content(@note.content, "note-link") %></p></div>
|
><p class="inline"><%= display_links(@note) %></p></div>
|
||||||
|
<div :if={!@note.content} class="text-sm italic text-center text-gray-600">
|
||||||
|
<%= gettext("(This note is empty)") %>
|
||||||
|
</div>
|
||||||
|
@ -0,0 +1,8 @@
|
|||||||
|
<div
|
||||||
|
:if={@pipeline.description}
|
||||||
|
id={"show-pipeline-description-#{@pipeline.id}"}
|
||||||
|
class="inline-block overflow-y-auto overflow-x-hidden h-32 whitespace-pre-wrap resize-y input input-primary min-h-32"
|
||||||
|
phx-update="ignore"
|
||||||
|
readonly
|
||||||
|
phx-no-format
|
||||||
|
><p class="inline"><%= display_links(@pipeline) %></p></div>
|
@ -1,8 +1,8 @@
|
|||||||
<div
|
<div
|
||||||
|
:if={@step.content}
|
||||||
id={"show-step-content-#{@step.id}"}
|
id={"show-step-content-#{@step.id}"}
|
||||||
class="input input-primary h-32 min-h-32 inline-block whitespace-pre-wrap overflow-x-hidden overflow-y-auto"
|
class="inline-block overflow-y-auto overflow-x-hidden h-32 whitespace-pre-wrap resize-y input input-primary min-h-32"
|
||||||
phx-hook="MaintainAttrs"
|
|
||||||
phx-update="ignore"
|
phx-update="ignore"
|
||||||
readonly
|
readonly
|
||||||
phx-no-format
|
phx-no-format
|
||||||
><p class="inline"><%= add_links_to_content(@step.content, "step-context") %></p></div>
|
><p class="inline"><%= display_links(@step) %></p></div>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
<label for={@id || @action} class="inline-flex relative items-center cursor-pointer">
|
<label for={@id || @action} class="relative inline-flex items-center cursor-pointer">
|
||||||
<input
|
<input
|
||||||
id={@id || @action}
|
id={@id || @action}
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
@ -23,7 +23,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<span
|
<span
|
||||||
id={"#{@id || @action}-label"}
|
id={"#{@id || @action}-label"}
|
||||||
class="ml-3 text-sm font-medium text-gray-900 dark:text-gray-300"
|
class="ml-3 text-sm font-medium text-gray-900 dark:text-gray-300 whitespace-nowrap"
|
||||||
>
|
>
|
||||||
<%= render_slot(@inner_block) %>
|
<%= render_slot(@inner_block) %>
|
||||||
</span>
|
</span>
|
||||||
|
@ -1,10 +1,7 @@
|
|||||||
<nav role="navigation" class="mb-8 px-8 py-4 w-full bg-primary-900 text-primary-400">
|
<nav role="navigation" class="mb-8 px-8 py-4 w-full bg-primary-900 text-primary-400">
|
||||||
<div class="flex flex-col sm:flex-row justify-between items-center">
|
<div class="flex flex-col sm:flex-row justify-between items-center">
|
||||||
<div class="mb-4 sm:mb-0 sm:mr-8 flex flex-row justify-start items-center space-x-2">
|
<div class="mb-4 sm:mb-0 sm:mr-8 flex flex-row justify-start items-center space-x-2">
|
||||||
<.link
|
<.link navigate={~p"/"} class="mx-2 my-1 leading-5 text-xl text-primary-400 hover:underline">
|
||||||
navigate={Routes.live_path(Endpoint, HomeLive)}
|
|
||||||
class="mx-2 my-1 leading-5 text-xl text-primary-400 hover:underline"
|
|
||||||
>
|
|
||||||
<%= gettext("memEx") %>
|
<%= gettext("memEx") %>
|
||||||
</.link>
|
</.link>
|
||||||
|
|
||||||
@ -21,28 +18,19 @@
|
|||||||
<ul class="flex flex-row flex-wrap justify-center items-center
|
<ul class="flex flex-row flex-wrap justify-center items-center
|
||||||
text-lg text-primary-400 text-ellipsis">
|
text-lg text-primary-400 text-ellipsis">
|
||||||
<li class="mx-2 my-1">
|
<li class="mx-2 my-1">
|
||||||
<.link
|
<.link navigate={~p"/notes"} class="text-primary-400 hover:underline truncate">
|
||||||
navigate={Routes.note_index_path(Endpoint, :index)}
|
|
||||||
class="text-primary-400 hover:underline truncate"
|
|
||||||
>
|
|
||||||
<%= gettext("notes") %>
|
<%= gettext("notes") %>
|
||||||
</.link>
|
</.link>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li class="mx-2 my-1">
|
<li class="mx-2 my-1">
|
||||||
<.link
|
<.link navigate={~p"/contexts"} class="text-primary-400 hover:underline truncate">
|
||||||
navigate={Routes.context_index_path(Endpoint, :index)}
|
|
||||||
class="text-primary-400 hover:underline truncate"
|
|
||||||
>
|
|
||||||
<%= gettext("contexts") %>
|
<%= gettext("contexts") %>
|
||||||
</.link>
|
</.link>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li class="mx-2 my-1">
|
<li class="mx-2 my-1">
|
||||||
<.link
|
<.link navigate={~p"/pipelines"} class="text-primary-400 hover:underline truncate">
|
||||||
navigate={Routes.pipeline_index_path(Endpoint, :index)}
|
|
||||||
class="text-primary-400 hover:underline truncate"
|
|
||||||
>
|
|
||||||
<%= gettext("pipelines") %>
|
<%= gettext("pipelines") %>
|
||||||
</.link>
|
</.link>
|
||||||
</li>
|
</li>
|
||||||
@ -50,26 +38,20 @@
|
|||||||
<li class="mx-2 my-1 border-left border border-primary-700"></li>
|
<li class="mx-2 my-1 border-left border border-primary-700"></li>
|
||||||
|
|
||||||
<%= if @current_user do %>
|
<%= if @current_user do %>
|
||||||
<li :if={@current_user |> Accounts.is_already_admin?()} class="mx-2 my-1">
|
<li :if={@current_user |> Accounts.already_admin?()} class="mx-2 my-1">
|
||||||
<.link
|
<.link navigate={~p"/invites"} class="text-primary-400 hover:underline">
|
||||||
navigate={Routes.invite_index_path(Endpoint, :index)}
|
|
||||||
class="text-primary-400 hover:underline"
|
|
||||||
>
|
|
||||||
<%= gettext("invites") %>
|
<%= gettext("invites") %>
|
||||||
</.link>
|
</.link>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li class="mx-2 my-1">
|
<li class="mx-2 my-1">
|
||||||
<.link
|
<.link navigate={~p"/users/settings"} class="text-primary-400 hover:underline truncate">
|
||||||
navigate={Routes.user_settings_path(Endpoint, :edit)}
|
|
||||||
class="text-primary-400 hover:underline truncate"
|
|
||||||
>
|
|
||||||
<%= @current_user.email %>
|
<%= @current_user.email %>
|
||||||
</.link>
|
</.link>
|
||||||
</li>
|
</li>
|
||||||
<li class="mx-2 my-1">
|
<li class="mx-2 my-1">
|
||||||
<.link
|
<.link
|
||||||
href={Routes.user_session_path(Endpoint, :delete)}
|
href={~p"/users/log_out"}
|
||||||
method="delete"
|
method="delete"
|
||||||
data-confirm={dgettext("prompts", "are you sure you want to log out?")}
|
data-confirm={dgettext("prompts", "are you sure you want to log out?")}
|
||||||
aria-label={gettext("log out")}
|
aria-label={gettext("log out")}
|
||||||
@ -84,7 +66,7 @@
|
|||||||
class="mx-2 my-1"
|
class="mx-2 my-1"
|
||||||
>
|
>
|
||||||
<.link
|
<.link
|
||||||
navigate={Routes.live_dashboard_path(Endpoint, :home)}
|
navigate={~p"/dashboard"}
|
||||||
class="text-primary-400 hover:underline"
|
class="text-primary-400 hover:underline"
|
||||||
aria-label={gettext("live dashboard")}
|
aria-label={gettext("live dashboard")}
|
||||||
>
|
>
|
||||||
@ -93,19 +75,13 @@
|
|||||||
</li>
|
</li>
|
||||||
<% else %>
|
<% else %>
|
||||||
<li :if={Accounts.allow_registration?()} class="mx-2 my-1">
|
<li :if={Accounts.allow_registration?()} class="mx-2 my-1">
|
||||||
<.link
|
<.link href={~p"/users/register"} class="text-primary-400 hover:underline truncate">
|
||||||
href={Routes.user_registration_path(Endpoint, :new)}
|
|
||||||
class="text-primary-400 hover:underline truncate"
|
|
||||||
>
|
|
||||||
<%= dgettext("actions", "register") %>
|
<%= dgettext("actions", "register") %>
|
||||||
</.link>
|
</.link>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li class="mx-2 my-1">
|
<li class="mx-2 my-1">
|
||||||
<.link
|
<.link href={~p"/users/log_in"} class="text-primary-400 hover:underline truncate">
|
||||||
href={Routes.user_session_path(Endpoint, :new)}
|
|
||||||
class="text-primary-400 hover:underline truncate"
|
|
||||||
>
|
|
||||||
<%= dgettext("actions", "log in") %>
|
<%= dgettext("actions", "log in") %>
|
||||||
</.link>
|
</.link>
|
||||||
</li>
|
</li>
|
||||||
|
17
lib/memex_web/components/layouts.ex
Normal file
17
lib/memex_web/components/layouts.ex
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
defmodule MemexWeb.Layouts do
|
||||||
|
@moduledoc """
|
||||||
|
The root layouts for the entire application
|
||||||
|
"""
|
||||||
|
|
||||||
|
use MemexWeb, :html
|
||||||
|
|
||||||
|
embed_templates "layouts/*"
|
||||||
|
|
||||||
|
def get_title(%{assigns: %{title: title}}) when title not in [nil, ""] do
|
||||||
|
gettext("memEx | %{title}", title: title)
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_title(_conn) do
|
||||||
|
gettext("memEx")
|
||||||
|
end
|
||||||
|
end
|
@ -1,26 +1,26 @@
|
|||||||
<main class="pb-8 min-w-full">
|
<main role="main" class="pb-8 min-w-full">
|
||||||
<header>
|
<header>
|
||||||
<.topbar current_user={assigns[:current_user]} />
|
<.topbar current_user={assigns[:current_user]} />
|
||||||
|
|
||||||
<div class="mx-8 my-2 flex flex-col space-y-4 text-center">
|
<div class="mx-8 my-2 flex flex-col space-y-4 text-center">
|
||||||
<p
|
<p
|
||||||
:if={@flash && @flash |> Map.has_key?("info")}
|
:if={@flash && @flash |> Map.has_key?("info")}
|
||||||
class="alert alert-info"
|
class="alert alert-info cursor-pointer"
|
||||||
role="alert"
|
role="alert"
|
||||||
phx-click="lv:clear-flash"
|
phx-click="lv:clear-flash"
|
||||||
phx-value-key="info"
|
phx-value-key="info"
|
||||||
>
|
>
|
||||||
<%= live_flash(@flash, "info") %>
|
<%= Phoenix.Flash.get(@flash, :info) %>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p
|
<p
|
||||||
:if={@flash && @flash |> Map.has_key?("error")}
|
:if={@flash && @flash |> Map.has_key?("error")}
|
||||||
class="alert alert-danger"
|
class="alert alert-danger cursor-pointer"
|
||||||
role="alert"
|
role="alert"
|
||||||
phx-click="lv:clear-flash"
|
phx-click="lv:clear-flash"
|
||||||
phx-value-key="error"
|
phx-value-key="error"
|
||||||
>
|
>
|
||||||
<%= live_flash(@flash, "error") %>
|
<%= Phoenix.Flash.get(@flash, :error) %>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
@ -32,7 +32,7 @@
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
id="disconnect"
|
id="disconnect"
|
||||||
class="z-50 fixed opacity-0 bottom-12 right-12 px-8 py-4 w-max h-max
|
class="z-50 fixed opacity-0 bottom-8 right-12 px-8 py-4 w-max h-max
|
||||||
border border-primary-400 shadow-lg rounded-lg bg-primary-900 text-primary-400
|
border border-primary-400 shadow-lg rounded-lg bg-primary-900 text-primary-400
|
||||||
flex justify-center items-center space-x-4
|
flex justify-center items-center space-x-4
|
||||||
transition-opacity ease-in-out duration-500 delay-[2000ms]"
|
transition-opacity ease-in-out duration-500 delay-[2000ms]"
|
@ -9,11 +9,8 @@
|
|||||||
|
|
||||||
<hr style="margin: 2em auto; border-width: 1px; border-color: rgb(161, 161, 170); width: 100%; max-width: 42rem;" />
|
<hr style="margin: 2em auto; border-width: 1px; border-color: rgb(161, 161, 170); width: 100%; max-width: 42rem;" />
|
||||||
|
|
||||||
<a style="color: rgb(161, 161, 170);" href={Routes.live_url(Endpoint, HomeLive)}>
|
<a style="color: rgb(161, 161, 170);" href={~p"/"}>
|
||||||
<%= dgettext(
|
<%= dgettext("emails", "this email was sent from memEx") %>
|
||||||
"emails",
|
|
||||||
"This email was sent from memEx"
|
|
||||||
) %>
|
|
||||||
</a>
|
</a>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
9
lib/memex_web/components/layouts/email_text.txt.eex
Normal file
9
lib/memex_web/components/layouts/email_text.txt.eex
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<%= @email.subject %>
|
||||||
|
|
||||||
|
====================
|
||||||
|
|
||||||
|
<%= @inner_content %>
|
||||||
|
|
||||||
|
=====================
|
||||||
|
|
||||||
|
<%= dgettext("emails", "this email was sent from memEx at %{url}", url: ~p"/") %>
|
@ -8,13 +8,8 @@
|
|||||||
<.live_title suffix={" | #{gettext("memEx")}"}>
|
<.live_title suffix={" | #{gettext("memEx")}"}>
|
||||||
<%= assigns[:page_title] || gettext("memEx") %>
|
<%= assigns[:page_title] || gettext("memEx") %>
|
||||||
</.live_title>
|
</.live_title>
|
||||||
<link phx-track-static rel="stylesheet" href={Routes.static_path(@conn, "/css/app.css")} />
|
<link phx-track-static rel="stylesheet" href={~p"/css/app.css"} />
|
||||||
<script
|
<script defer phx-track-static type="text/javascript" src={~p"/js/app.js"}>
|
||||||
defer
|
|
||||||
phx-track-static
|
|
||||||
type="text/javascript"
|
|
||||||
src={Routes.static_path(@conn, "/js/app.js")}
|
|
||||||
>
|
|
||||||
</script>
|
</script>
|
||||||
</head>
|
</head>
|
||||||
|
|
@ -37,7 +37,7 @@ defmodule MemexWeb.Components.NotesTableComponent do
|
|||||||
} = socket
|
} = socket
|
||||||
) do
|
) do
|
||||||
columns =
|
columns =
|
||||||
if actions == [] or current_user |> is_nil() do
|
if actions == [] or !current_user do
|
||||||
[]
|
[]
|
||||||
else
|
else
|
||||||
[%{label: gettext("actions"), key: :actions, sortable: false}]
|
[%{label: gettext("actions"), key: :actions, sortable: false}]
|
||||||
@ -88,11 +88,9 @@ defmodule MemexWeb.Components.NotesTableComponent do
|
|||||||
|
|
||||||
@spec get_value_for_key(atom(), Note.t(), additional_data :: map()) ::
|
@spec get_value_for_key(atom(), Note.t(), additional_data :: map()) ::
|
||||||
any() | {any(), Rendered.t()}
|
any() | {any(), Rendered.t()}
|
||||||
defp get_value_for_key(:slug, %{slug: slug}, _additional_data) do
|
defp get_value_for_key(:slug, %{slug: slug} = assigns, _additional_data) do
|
||||||
assigns = %{slug: slug}
|
|
||||||
|
|
||||||
slug_block = ~H"""
|
slug_block = ~H"""
|
||||||
<.link navigate={Routes.note_show_path(Endpoint, :show, @slug)} class="link">
|
<.link navigate={~p"/note/#{@slug}"} class="link">
|
||||||
<%= @slug %>
|
<%= @slug %>
|
||||||
</.link>
|
</.link>
|
||||||
"""
|
"""
|
||||||
@ -100,12 +98,10 @@ defmodule MemexWeb.Components.NotesTableComponent do
|
|||||||
{slug, slug_block}
|
{slug, slug_block}
|
||||||
end
|
end
|
||||||
|
|
||||||
defp get_value_for_key(:tags, %{tags: tags}, _additional_data) do
|
defp get_value_for_key(:tags, assigns, _additional_data) do
|
||||||
assigns = %{tags: tags}
|
|
||||||
|
|
||||||
~H"""
|
~H"""
|
||||||
<div class="flex flex-wrap justify-center space-x-1">
|
<div class="flex flex-wrap justify-center space-x-1">
|
||||||
<.link :for={tag <- @tags} patch={Routes.note_index_path(Endpoint, :search, tag)} class="link">
|
<.link :for={tag <- @tags} patch={~p"/notes/#{tag}"} class="link">
|
||||||
<%= tag %>
|
<%= tag %>
|
||||||
</.link>
|
</.link>
|
||||||
</div>
|
</div>
|
||||||
|
@ -37,7 +37,7 @@ defmodule MemexWeb.Components.PipelinesTableComponent do
|
|||||||
} = socket
|
} = socket
|
||||||
) do
|
) do
|
||||||
columns =
|
columns =
|
||||||
if actions == [] or current_user |> is_nil() do
|
if actions == [] or !current_user do
|
||||||
[]
|
[]
|
||||||
else
|
else
|
||||||
[%{label: gettext("actions"), key: :actions, sortable: false}]
|
[%{label: gettext("actions"), key: :actions, sortable: false}]
|
||||||
@ -89,11 +89,9 @@ defmodule MemexWeb.Components.PipelinesTableComponent do
|
|||||||
|
|
||||||
@spec get_value_for_key(atom(), Pipeline.t(), additional_data :: map()) ::
|
@spec get_value_for_key(atom(), Pipeline.t(), additional_data :: map()) ::
|
||||||
any() | {any(), Rendered.t()}
|
any() | {any(), Rendered.t()}
|
||||||
defp get_value_for_key(:slug, %{slug: slug}, _additional_data) do
|
defp get_value_for_key(:slug, %{slug: slug} = assigns, _additional_data) do
|
||||||
assigns = %{slug: slug}
|
|
||||||
|
|
||||||
slug_block = ~H"""
|
slug_block = ~H"""
|
||||||
<.link navigate={Routes.pipeline_show_path(Endpoint, :show, @slug)} class="link">
|
<.link navigate={~p"/pipeline/#{@slug}"} class="link">
|
||||||
<%= @slug %>
|
<%= @slug %>
|
||||||
</.link>
|
</.link>
|
||||||
"""
|
"""
|
||||||
@ -101,11 +99,9 @@ defmodule MemexWeb.Components.PipelinesTableComponent do
|
|||||||
{slug, slug_block}
|
{slug, slug_block}
|
||||||
end
|
end
|
||||||
|
|
||||||
defp get_value_for_key(:description, %{description: description}, _additional_data) do
|
defp get_value_for_key(:description, %{description: description} = assigns, _additional_data) do
|
||||||
assigns = %{description: description}
|
|
||||||
|
|
||||||
description_block = ~H"""
|
description_block = ~H"""
|
||||||
<div class="truncate max-w-sm">
|
<div class="max-w-sm truncate">
|
||||||
<%= @description %>
|
<%= @description %>
|
||||||
</div>
|
</div>
|
||||||
"""
|
"""
|
||||||
@ -113,16 +109,10 @@ defmodule MemexWeb.Components.PipelinesTableComponent do
|
|||||||
{description, description_block}
|
{description, description_block}
|
||||||
end
|
end
|
||||||
|
|
||||||
defp get_value_for_key(:tags, %{tags: tags}, _additional_data) do
|
defp get_value_for_key(:tags, assigns, _additional_data) do
|
||||||
assigns = %{tags: tags}
|
|
||||||
|
|
||||||
~H"""
|
~H"""
|
||||||
<div class="flex flex-wrap justify-center space-x-1">
|
<div class="flex flex-wrap justify-center space-x-1">
|
||||||
<.link
|
<.link :for={tag <- @tags} patch={~p"/pipelines/#{tag}"} class="link">
|
||||||
:for={tag <- @tags}
|
|
||||||
patch={Routes.pipeline_index_path(Endpoint, :search, tag)}
|
|
||||||
class="link"
|
|
||||||
>
|
|
||||||
<%= tag %>
|
<%= tag %>
|
||||||
</.link>
|
</.link>
|
||||||
</div>
|
</div>
|
||||||
|
@ -135,4 +135,25 @@ defmodule MemexWeb.Components.TableComponent do
|
|||||||
sort_mode
|
sort_mode
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Conditionally composes elements into the columns list, supports maps and
|
||||||
|
lists. Works tail to front in order for efficiency
|
||||||
|
|
||||||
|
iex> []
|
||||||
|
...> |> maybe_compose_columns(%{label: "Column 3"}, true)
|
||||||
|
...> |> maybe_compose_columns(%{label: "Column 2"}, false)
|
||||||
|
...> |> maybe_compose_columns(%{label: "Column 1"})
|
||||||
|
[%{label: "Column 1"}, %{label: "Column 3"}]
|
||||||
|
|
||||||
|
"""
|
||||||
|
@spec maybe_compose_columns(list(), element_to_add :: list() | map()) :: list()
|
||||||
|
@spec maybe_compose_columns(list(), element_to_add :: list() | map(), boolean()) :: list()
|
||||||
|
def maybe_compose_columns(columns, element_or_elements, add? \\ true)
|
||||||
|
|
||||||
|
def maybe_compose_columns(columns, elements, true) when is_list(elements),
|
||||||
|
do: Enum.concat(elements, columns)
|
||||||
|
|
||||||
|
def maybe_compose_columns(columns, element, true) when is_map(element), do: [element | columns]
|
||||||
|
def maybe_compose_columns(columns, _element_or_elements, false), do: columns
|
||||||
end
|
end
|
||||||
|
13
lib/memex_web/controller_helpers.ex
Normal file
13
lib/memex_web/controller_helpers.ex
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
defmodule MemexWeb.ControllerHelpers do
|
||||||
|
@moduledoc """
|
||||||
|
Implements controller helpers
|
||||||
|
"""
|
||||||
|
|
||||||
|
import Plug.Conn, only: [assign: 3]
|
||||||
|
|
||||||
|
def assign(conn, assigns) do
|
||||||
|
assigns
|
||||||
|
|> Map.new()
|
||||||
|
|> Enum.reduce(conn, fn {key, value}, conn -> conn |> assign(key, value) end)
|
||||||
|
end
|
||||||
|
end
|
@ -6,7 +6,8 @@ defmodule MemexWeb.EmailController do
|
|||||||
use MemexWeb, :controller
|
use MemexWeb, :controller
|
||||||
alias Memex.Accounts.User
|
alias Memex.Accounts.User
|
||||||
|
|
||||||
plug :put_layout, {MemexWeb.LayoutView, :email}
|
plug :put_root_layout, html: {MemexWeb.Layouts, :email_html}
|
||||||
|
plug :put_layout, false
|
||||||
|
|
||||||
@sample_assigns %{
|
@sample_assigns %{
|
||||||
email: %{subject: "Example subject"},
|
email: %{subject: "Example subject"},
|
||||||
@ -18,6 +19,6 @@ defmodule MemexWeb.EmailController do
|
|||||||
Debug route used to preview emails
|
Debug route used to preview emails
|
||||||
"""
|
"""
|
||||||
def preview(conn, %{"id" => template}) do
|
def preview(conn, %{"id" => template}) do
|
||||||
render(conn, "#{template |> to_string()}.html", @sample_assigns)
|
render(conn, String.to_existing_atom(template), @sample_assigns)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
10
lib/memex_web/controllers/email_html.ex
Normal file
10
lib/memex_web/controllers/email_html.ex
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
defmodule MemexWeb.EmailHTML do
|
||||||
|
@moduledoc """
|
||||||
|
Renders email templates
|
||||||
|
"""
|
||||||
|
|
||||||
|
use MemexWeb, :html
|
||||||
|
|
||||||
|
embed_templates "email_html/*.html", suffix: "_html"
|
||||||
|
embed_templates "email_html/*.txt", suffix: "_text"
|
||||||
|
end
|
@ -15,7 +15,7 @@
|
|||||||
|
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
<a style="margin: 1em; color: rgb(31, 31, 31);" href={@url}><%= @url %></a>
|
<a style="margin: 1em; color: rgb(161, 161, 170);" href={@url}><%= @url %></a>
|
||||||
|
|
||||||
<br />
|
<br />
|
||||||
|
|
@ -9,4 +9,4 @@
|
|||||||
|
|
||||||
<%= dgettext("emails",
|
<%= dgettext("emails",
|
||||||
"If you didn't create an account at %{url}, please ignore this.",
|
"If you didn't create an account at %{url}, please ignore this.",
|
||||||
url: Routes.live_url(Endpoint, HomeLive)) %>
|
url: ~p"/") %>
|
@ -9,7 +9,7 @@
|
|||||||
|
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
<a style="margin: 1em; color: rgb(31, 31, 31);" href={@url}><%= @url %></a>
|
<a style="margin: 1em; color: rgb(161, 161, 170);" href={@url}><%= @url %></a>
|
||||||
|
|
||||||
<br />
|
<br />
|
||||||
|
|
@ -7,4 +7,4 @@
|
|||||||
|
|
||||||
<%= dgettext("emails",
|
<%= dgettext("emails",
|
||||||
"If you didn't request this change from %{url}, please ignore this.",
|
"If you didn't request this change from %{url}, please ignore this.",
|
||||||
url: Routes.live_url(Endpoint, HomeLive)) %>
|
url: ~p"/") %>
|
@ -9,7 +9,7 @@
|
|||||||
|
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
<a style="margin: 1em; color: rgb(31, 31, 31);" href={@url}><%= @url %></a>
|
<a style="margin: 1em; color: rgb(161, 161, 170);" href={@url}><%= @url %></a>
|
||||||
|
|
||||||
<br />
|
<br />
|
||||||
|
|
@ -7,4 +7,4 @@
|
|||||||
|
|
||||||
<%= dgettext("emails",
|
<%= dgettext("emails",
|
||||||
"If you didn't request this change from %{url}, please ignore this.",
|
"If you didn't request this change from %{url}, please ignore this.",
|
||||||
url: Routes.live_url(Endpoint, HomeLive)) %>
|
url: ~p"/") %>
|
@ -1,15 +1,16 @@
|
|||||||
defmodule MemexWeb.ErrorView do
|
defmodule MemexWeb.ErrorHTML do
|
||||||
use MemexWeb, :view
|
use MemexWeb, :html
|
||||||
alias MemexWeb.HomeLive
|
|
||||||
|
|
||||||
def template_not_found(error_path, _assigns) do
|
embed_templates "error_html/*"
|
||||||
|
|
||||||
|
def render(template, _assigns) do
|
||||||
error_string =
|
error_string =
|
||||||
case error_path do
|
case template do
|
||||||
"404.html" -> dgettext("errors", "not found")
|
"404.html" -> dgettext("errors", "not found")
|
||||||
"401.html" -> dgettext("errors", "unauthorized")
|
"401.html" -> dgettext("errors", "unauthorized")
|
||||||
_other_path -> dgettext("errors", "internal server error")
|
_other_path -> dgettext("errors", "internal server error")
|
||||||
end
|
end
|
||||||
|
|
||||||
render("error.html", %{error_string: error_string})
|
error(%{error_string: error_string})
|
||||||
end
|
end
|
||||||
end
|
end
|
@ -24,10 +24,7 @@
|
|||||||
|
|
||||||
<hr class="w-full hr" />
|
<hr class="w-full hr" />
|
||||||
|
|
||||||
<.link
|
<.link href={~p"/"} class="link title text-primary-400 text-lg">
|
||||||
href={Routes.live_path(Endpoint, HomeLive)}
|
|
||||||
class="link title text-primary-400 text-lg"
|
|
||||||
>
|
|
||||||
<%= dgettext("errors", "go back home") %>
|
<%= dgettext("errors", "go back home") %>
|
||||||
</.link>
|
</.link>
|
||||||
</div>
|
</div>
|
14
lib/memex_web/controllers/error_json.ex
Normal file
14
lib/memex_web/controllers/error_json.ex
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
defmodule MemexWeb.ErrorJSON do
|
||||||
|
use Gettext, backend: MemexWeb.Gettext
|
||||||
|
|
||||||
|
def render(template, _assigns) do
|
||||||
|
error_string =
|
||||||
|
case template do
|
||||||
|
"404.json" -> dgettext("errors", "not found")
|
||||||
|
"401.json" -> dgettext("errors", "unauthorized")
|
||||||
|
_other_path -> dgettext("errors", "internal server error")
|
||||||
|
end
|
||||||
|
|
||||||
|
%{errors: %{detail: error_string}}
|
||||||
|
end
|
||||||
|
end
|
@ -1,11 +0,0 @@
|
|||||||
defmodule MemexWeb.HomeController do
|
|
||||||
@moduledoc """
|
|
||||||
Controller for home page
|
|
||||||
"""
|
|
||||||
|
|
||||||
use MemexWeb, :controller
|
|
||||||
|
|
||||||
def index(conn, _params) do
|
|
||||||
render(conn, "index.html")
|
|
||||||
end
|
|
||||||
end
|
|
5
lib/memex_web/controllers/home_html.ex
Normal file
5
lib/memex_web/controllers/home_html.ex
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
defmodule MemexWeb.HomeHTML do
|
||||||
|
use MemexWeb, :html
|
||||||
|
|
||||||
|
embed_templates "home_html/*"
|
||||||
|
end
|
@ -3,12 +3,11 @@ defmodule MemexWeb.UserAuth do
|
|||||||
Functions for user session and authentication
|
Functions for user session and authentication
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
use MemexWeb, :verified_routes
|
||||||
|
use Gettext, backend: MemexWeb.Gettext
|
||||||
import Plug.Conn
|
import Plug.Conn
|
||||||
import Phoenix.Controller
|
import Phoenix.Controller
|
||||||
import MemexWeb.Gettext
|
|
||||||
alias Memex.{Accounts, Accounts.User}
|
alias Memex.{Accounts, Accounts.User}
|
||||||
alias MemexWeb.HomeLive
|
|
||||||
alias MemexWeb.Router.Helpers, as: Routes
|
|
||||||
|
|
||||||
# Make the remember me cookie valid for 60 days.
|
# Make the remember me cookie valid for 60 days.
|
||||||
# If you want bump or reduce this value, also change
|
# If you want bump or reduce this value, also change
|
||||||
@ -39,7 +38,7 @@ defmodule MemexWeb.UserAuth do
|
|||||||
dgettext("errors", "You must confirm your account and log in to access this page.")
|
dgettext("errors", "You must confirm your account and log in to access this page.")
|
||||||
)
|
)
|
||||||
|> maybe_store_return_to()
|
|> maybe_store_return_to()
|
||||||
|> redirect(to: Routes.user_session_path(conn, :new))
|
|> redirect(to: ~p"/users/log_in")
|
||||||
|> halt()
|
|> halt()
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -49,8 +48,7 @@ defmodule MemexWeb.UserAuth do
|
|||||||
|
|
||||||
conn
|
conn
|
||||||
|> renew_session()
|
|> renew_session()
|
||||||
|> put_session(:user_token, token)
|
|> put_token_in_session(token)
|
||||||
|> put_session(:live_socket_id, "users_sessions:#{Base.url_encode64(token)}")
|
|
||||||
|> maybe_write_remember_me_cookie(token, params)
|
|> maybe_write_remember_me_cookie(token, params)
|
||||||
|> redirect(to: user_return_to || signed_in_path(conn))
|
|> redirect(to: user_return_to || signed_in_path(conn))
|
||||||
end
|
end
|
||||||
@ -96,7 +94,7 @@ defmodule MemexWeb.UserAuth do
|
|||||||
"""
|
"""
|
||||||
def log_out_user(conn) do
|
def log_out_user(conn) do
|
||||||
user_token = get_session(conn, :user_token)
|
user_token = get_session(conn, :user_token)
|
||||||
user_token && Accounts.delete_session_token(user_token)
|
user_token && Accounts.delete_user_session_token(user_token)
|
||||||
|
|
||||||
if live_socket_id = get_session(conn, :live_socket_id) do
|
if live_socket_id = get_session(conn, :live_socket_id) do
|
||||||
MemexWeb.Endpoint.broadcast(live_socket_id, "disconnect", %{})
|
MemexWeb.Endpoint.broadcast(live_socket_id, "disconnect", %{})
|
||||||
@ -105,7 +103,7 @@ defmodule MemexWeb.UserAuth do
|
|||||||
conn
|
conn
|
||||||
|> renew_session()
|
|> renew_session()
|
||||||
|> delete_resp_cookie(@remember_me_cookie)
|
|> delete_resp_cookie(@remember_me_cookie)
|
||||||
|> redirect(to: "/")
|
|> redirect(to: ~p"/")
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
@ -119,19 +117,110 @@ defmodule MemexWeb.UserAuth do
|
|||||||
end
|
end
|
||||||
|
|
||||||
defp ensure_user_token(conn) do
|
defp ensure_user_token(conn) do
|
||||||
if user_token = get_session(conn, :user_token) do
|
if token = get_session(conn, :user_token) do
|
||||||
{user_token, conn}
|
{token, conn}
|
||||||
else
|
else
|
||||||
conn = fetch_cookies(conn, signed: [@remember_me_cookie])
|
conn = fetch_cookies(conn, signed: [@remember_me_cookie])
|
||||||
|
|
||||||
if user_token = conn.cookies[@remember_me_cookie] do
|
if token = conn.cookies[@remember_me_cookie] do
|
||||||
{user_token, put_session(conn, :user_token, user_token)}
|
{token, put_token_in_session(conn, token)}
|
||||||
else
|
else
|
||||||
{nil, conn}
|
{nil, conn}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Handles mounting and authenticating the current_user in LiveViews.
|
||||||
|
|
||||||
|
## `on_mount` arguments
|
||||||
|
|
||||||
|
* `:mount_current_user` - Assigns current_user
|
||||||
|
to socket assigns based on user_token, or nil if
|
||||||
|
there's no user_token or no matching user.
|
||||||
|
|
||||||
|
* `:ensure_authenticated` - Authenticates the user from the session,
|
||||||
|
and assigns the current_user to socket assigns based
|
||||||
|
on user_token.
|
||||||
|
Redirects to login page if there's no logged user.
|
||||||
|
|
||||||
|
* `:redirect_if_user_is_authenticated` - Authenticates the user from the session.
|
||||||
|
Redirects to signed_in_path if there's a logged user.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
Use the `on_mount` lifecycle macro in LiveViews to mount or authenticate
|
||||||
|
the current_user:
|
||||||
|
|
||||||
|
defmodule MemexWeb.PageLive do
|
||||||
|
use MemexWeb, :live_view
|
||||||
|
|
||||||
|
on_mount {MemexWeb.UserAuth, :mount_current_user}
|
||||||
|
...
|
||||||
|
end
|
||||||
|
|
||||||
|
Or use the `live_session` of your router to invoke the on_mount callback:
|
||||||
|
|
||||||
|
live_session :authenticated, on_mount: [{MemexWeb.UserAuth, :ensure_authenticated}] do
|
||||||
|
live "/profile", ProfileLive, :index
|
||||||
|
end
|
||||||
|
"""
|
||||||
|
def on_mount(:mount_current_user, _params, session, socket) do
|
||||||
|
{:cont, mount_current_user(session, socket)}
|
||||||
|
end
|
||||||
|
|
||||||
|
def on_mount(:ensure_authenticated, _params, session, socket) do
|
||||||
|
socket = mount_current_user(session, socket)
|
||||||
|
|
||||||
|
if socket.assigns.current_user do
|
||||||
|
{:cont, socket}
|
||||||
|
else
|
||||||
|
error_flash = dgettext("errors", "You must log in to access this page.")
|
||||||
|
|
||||||
|
socket =
|
||||||
|
socket
|
||||||
|
|> Phoenix.LiveView.put_flash(:error, error_flash)
|
||||||
|
|> Phoenix.LiveView.redirect(to: ~p"/users/log_in")
|
||||||
|
|
||||||
|
{:halt, socket}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def on_mount(:ensure_admin, _params, session, socket) do
|
||||||
|
socket = mount_current_user(session, socket)
|
||||||
|
|
||||||
|
if socket.assigns.current_user && socket.assigns.current_user.role == :admin do
|
||||||
|
{:cont, socket}
|
||||||
|
else
|
||||||
|
error_flash = dgettext("errors", "You must log in as an administrator to access this page.")
|
||||||
|
|
||||||
|
socket =
|
||||||
|
socket
|
||||||
|
|> Phoenix.LiveView.put_flash(:error, error_flash)
|
||||||
|
|> Phoenix.LiveView.redirect(to: ~p"/users/log_in")
|
||||||
|
|
||||||
|
{:halt, socket}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def on_mount(:redirect_if_user_is_authenticated, _params, session, socket) do
|
||||||
|
socket = mount_current_user(session, socket)
|
||||||
|
|
||||||
|
if socket.assigns.current_user do
|
||||||
|
{:halt, Phoenix.LiveView.redirect(socket, to: signed_in_path(socket))}
|
||||||
|
else
|
||||||
|
{:cont, socket}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp mount_current_user(session, socket) do
|
||||||
|
Phoenix.Component.assign_new(socket, :current_user, fn ->
|
||||||
|
if user_token = session["user_token"] do
|
||||||
|
Accounts.get_user_by_session_token(user_token)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Used for routes that require the user to not be authenticated.
|
Used for routes that require the user to not be authenticated.
|
||||||
"""
|
"""
|
||||||
@ -161,7 +250,7 @@ defmodule MemexWeb.UserAuth do
|
|||||||
dgettext("errors", "You must confirm your account and log in to access this page.")
|
dgettext("errors", "You must confirm your account and log in to access this page.")
|
||||||
)
|
)
|
||||||
|> maybe_store_return_to()
|
|> maybe_store_return_to()
|
||||||
|> redirect(to: Routes.user_session_path(conn, :new))
|
|> redirect(to: ~p"/users/log_in")
|
||||||
|> halt()
|
|> halt()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@ -176,16 +265,34 @@ defmodule MemexWeb.UserAuth do
|
|||||||
conn
|
conn
|
||||||
|> put_flash(:error, dgettext("errors", "You are not authorized to view this page."))
|
|> put_flash(:error, dgettext("errors", "You are not authorized to view this page."))
|
||||||
|> maybe_store_return_to()
|
|> maybe_store_return_to()
|
||||||
|> redirect(to: Routes.live_path(conn, HomeLive))
|
|> redirect(to: ~p"/")
|
||||||
|> halt()
|
|> halt()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def put_user_locale(%{assigns: %{current_user: %{locale: locale}}} = conn, _opts) do
|
||||||
|
default = Application.fetch_env!(:gettext, :default_locale)
|
||||||
|
Gettext.put_locale(locale || default)
|
||||||
|
conn |> put_session(:locale, locale || default)
|
||||||
|
end
|
||||||
|
|
||||||
|
def put_user_locale(conn, _opts) do
|
||||||
|
default = Application.fetch_env!(:gettext, :default_locale)
|
||||||
|
Gettext.put_locale(default)
|
||||||
|
conn |> put_session(:locale, default)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp put_token_in_session(conn, token) do
|
||||||
|
conn
|
||||||
|
|> put_session(:user_token, token)
|
||||||
|
|> put_session(:live_socket_id, "users_sessions:#{Base.url_encode64(token)}")
|
||||||
|
end
|
||||||
|
|
||||||
defp maybe_store_return_to(%{method: "GET"} = conn) do
|
defp maybe_store_return_to(%{method: "GET"} = conn) do
|
||||||
put_session(conn, :user_return_to, current_path(conn))
|
put_session(conn, :user_return_to, current_path(conn))
|
||||||
end
|
end
|
||||||
|
|
||||||
defp maybe_store_return_to(conn), do: conn
|
defp maybe_store_return_to(conn), do: conn
|
||||||
|
|
||||||
defp signed_in_path(_conn), do: "/"
|
defp signed_in_path(_conn), do: ~p"/"
|
||||||
end
|
end
|
||||||
|
@ -1,18 +1,17 @@
|
|||||||
defmodule MemexWeb.UserConfirmationController do
|
defmodule MemexWeb.UserConfirmationController do
|
||||||
use MemexWeb, :controller
|
use MemexWeb, :controller
|
||||||
|
use Gettext, backend: MemexWeb.Gettext
|
||||||
import MemexWeb.Gettext
|
|
||||||
alias Memex.Accounts
|
alias Memex.Accounts
|
||||||
|
|
||||||
def new(conn, _params) do
|
def new(conn, _params) do
|
||||||
render(conn, "new.html", page_title: gettext("Confirm your account"))
|
render(conn, :new, page_title: gettext("confirm your account"))
|
||||||
end
|
end
|
||||||
|
|
||||||
def create(conn, %{"user" => %{"email" => email}}) do
|
def create(conn, %{"user" => %{"email" => email}}) do
|
||||||
if user = Accounts.get_user_by_email(email) do
|
if user = Accounts.get_user_by_email(email) do
|
||||||
Accounts.deliver_user_confirmation_instructions(
|
Accounts.deliver_user_confirmation_instructions(
|
||||||
user,
|
user,
|
||||||
&Routes.user_confirmation_url(conn, :confirm, &1)
|
fn token -> url(MemexWeb.Endpoint, ~p"/users/confirm/#{token}") end
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -22,11 +21,10 @@ defmodule MemexWeb.UserConfirmationController do
|
|||||||
:info,
|
:info,
|
||||||
dgettext(
|
dgettext(
|
||||||
"prompts",
|
"prompts",
|
||||||
"If your email is in our system and it has not been confirmed yet, " <>
|
"if your email is in our system and it has not been confirmed yet, you will receive an email with instructions shortly."
|
||||||
"you will receive an email with instructions shortly."
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|> redirect(to: "/")
|
|> redirect(to: ~p"/")
|
||||||
end
|
end
|
||||||
|
|
||||||
# Do not log in the user after confirmation to avoid a
|
# Do not log in the user after confirmation to avoid a
|
||||||
@ -36,7 +34,7 @@ defmodule MemexWeb.UserConfirmationController do
|
|||||||
{:ok, %{email: email}} ->
|
{:ok, %{email: email}} ->
|
||||||
conn
|
conn
|
||||||
|> put_flash(:info, dgettext("prompts", "%{email} confirmed successfully.", email: email))
|
|> put_flash(:info, dgettext("prompts", "%{email} confirmed successfully.", email: email))
|
||||||
|> redirect(to: "/")
|
|> redirect(to: ~p"/")
|
||||||
|
|
||||||
:error ->
|
:error ->
|
||||||
# If there is a current user and the account was already confirmed,
|
# If there is a current user and the account was already confirmed,
|
||||||
@ -44,16 +42,16 @@ defmodule MemexWeb.UserConfirmationController do
|
|||||||
# by some automation or by the user themselves, so we redirect without
|
# by some automation or by the user themselves, so we redirect without
|
||||||
# a warning message.
|
# a warning message.
|
||||||
case conn.assigns do
|
case conn.assigns do
|
||||||
%{current_user: %{confirmed_at: confirmed_at}} when not is_nil(confirmed_at) ->
|
%{current_user: %{confirmed_at: %{}}} ->
|
||||||
redirect(conn, to: "/")
|
redirect(conn, to: ~p"/")
|
||||||
|
|
||||||
%{} ->
|
%{} ->
|
||||||
conn
|
conn
|
||||||
|> put_flash(
|
|> put_flash(
|
||||||
:error,
|
:error,
|
||||||
dgettext("errors", "User confirmation link is invalid or it has expired.")
|
dgettext("errors", "user confirmation link is invalid or it has expired.")
|
||||||
)
|
)
|
||||||
|> redirect(to: "/")
|
|> redirect(to: ~p"/")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
6
lib/memex_web/controllers/user_confirmation_html.ex
Normal file
6
lib/memex_web/controllers/user_confirmation_html.ex
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
defmodule MemexWeb.UserConfirmationHTML do
|
||||||
|
use MemexWeb, :html
|
||||||
|
alias Memex.Accounts
|
||||||
|
|
||||||
|
embed_templates "user_confirmation_html/*"
|
||||||
|
end
|
@ -7,7 +7,7 @@
|
|||||||
:let={f}
|
:let={f}
|
||||||
for={%{}}
|
for={%{}}
|
||||||
as={:user}
|
as={:user}
|
||||||
action={Routes.user_confirmation_path(@conn, :create)}
|
action={~p"/users/confirm"}
|
||||||
class="flex flex-col space-y-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 justify-center items-center"
|
class="flex flex-col space-y-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 justify-center items-center"
|
||||||
>
|
>
|
||||||
<%= label(f, :email, gettext("Email"), class: "title text-lg text-primary-400") %>
|
<%= label(f, :email, gettext("Email"), class: "title text-lg text-primary-400") %>
|
||||||
@ -21,14 +21,10 @@
|
|||||||
<hr class="hr" />
|
<hr class="hr" />
|
||||||
|
|
||||||
<div class="flex flex-row justify-center items-center space-x-4">
|
<div class="flex flex-row justify-center items-center space-x-4">
|
||||||
<.link
|
<.link :if={Accounts.allow_registration?()} href={~p"/users/register"} class="btn btn-primary">
|
||||||
:if={Accounts.allow_registration?()}
|
|
||||||
href={Routes.user_registration_path(@conn, :new)}
|
|
||||||
class="btn btn-primary"
|
|
||||||
>
|
|
||||||
<%= dgettext("actions", "register") %>
|
<%= dgettext("actions", "register") %>
|
||||||
</.link>
|
</.link>
|
||||||
<.link href={Routes.user_session_path(@conn, :new)} class="btn btn-primary">
|
<.link href={~p"/users/log_in"} class="btn btn-primary">
|
||||||
<%= dgettext("actions", "log in") %>
|
<%= dgettext("actions", "log in") %>
|
||||||
</.link>
|
</.link>
|
||||||
</div>
|
</div>
|
@ -1,16 +1,16 @@
|
|||||||
defmodule MemexWeb.UserRegistrationController do
|
defmodule MemexWeb.UserRegistrationController do
|
||||||
use MemexWeb, :controller
|
use MemexWeb, :controller
|
||||||
import MemexWeb.Gettext
|
use Gettext, backend: MemexWeb.Gettext
|
||||||
|
alias Ecto.Changeset
|
||||||
alias Memex.{Accounts, Accounts.Invites}
|
alias Memex.{Accounts, Accounts.Invites}
|
||||||
alias MemexWeb.HomeLive
|
|
||||||
|
|
||||||
def new(conn, %{"invite" => invite_token}) do
|
def new(conn, %{"invite" => invite_token}) do
|
||||||
if Invites.valid_invite_token?(invite_token) do
|
if Invites.valid_invite_token?(invite_token) do
|
||||||
conn |> render_new(invite_token)
|
conn |> render_new(invite_token)
|
||||||
else
|
else
|
||||||
conn
|
conn
|
||||||
|> put_flash(:error, dgettext("errors", "Sorry, this invite was not found or expired"))
|
|> put_flash(:error, dgettext("errors", "sorry, this invite was not found or expired"))
|
||||||
|> redirect(to: Routes.live_path(Endpoint, HomeLive))
|
|> redirect(to: ~p"/")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -19,14 +19,14 @@ defmodule MemexWeb.UserRegistrationController do
|
|||||||
conn |> render_new()
|
conn |> render_new()
|
||||||
else
|
else
|
||||||
conn
|
conn
|
||||||
|> put_flash(:error, dgettext("errors", "Sorry, public registration is disabled"))
|
|> put_flash(:error, dgettext("errors", "sorry, public registration is disabled"))
|
||||||
|> redirect(to: Routes.live_path(Endpoint, HomeLive))
|
|> redirect(to: ~p"/")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# renders new user registration page
|
# renders new user registration page
|
||||||
defp render_new(conn, invite_token \\ nil) do
|
defp render_new(conn, invite_token \\ nil) do
|
||||||
render(conn, "new.html",
|
render(conn, :new,
|
||||||
changeset: Accounts.change_user_registration(),
|
changeset: Accounts.change_user_registration(),
|
||||||
invite_token: invite_token,
|
invite_token: invite_token,
|
||||||
page_title: gettext("register")
|
page_title: gettext("register")
|
||||||
@ -38,8 +38,8 @@ defmodule MemexWeb.UserRegistrationController do
|
|||||||
conn |> create_user(attrs, invite_token)
|
conn |> create_user(attrs, invite_token)
|
||||||
else
|
else
|
||||||
conn
|
conn
|
||||||
|> put_flash(:error, dgettext("errors", "Sorry, this invite was not found or expired"))
|
|> put_flash(:error, dgettext("errors", "sorry, this invite was not found or expired"))
|
||||||
|> redirect(to: Routes.live_path(Endpoint, HomeLive))
|
|> redirect(to: ~p"/")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -48,8 +48,8 @@ defmodule MemexWeb.UserRegistrationController do
|
|||||||
conn |> create_user(attrs)
|
conn |> create_user(attrs)
|
||||||
else
|
else
|
||||||
conn
|
conn
|
||||||
|> put_flash(:error, dgettext("errors", "Sorry, public registration is disabled"))
|
|> put_flash(:error, dgettext("errors", "sorry, public registration is disabled"))
|
||||||
|> redirect(to: Routes.live_path(Endpoint, HomeLive))
|
|> redirect(to: ~p"/")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -58,20 +58,20 @@ defmodule MemexWeb.UserRegistrationController do
|
|||||||
{:ok, user} ->
|
{:ok, user} ->
|
||||||
Accounts.deliver_user_confirmation_instructions(
|
Accounts.deliver_user_confirmation_instructions(
|
||||||
user,
|
user,
|
||||||
&Routes.user_confirmation_url(conn, :confirm, &1)
|
fn token -> url(MemexWeb.Endpoint, ~p"/users/confirm/#{token}") end
|
||||||
)
|
)
|
||||||
|
|
||||||
conn
|
conn
|
||||||
|> put_flash(:info, dgettext("prompts", "please check your email to verify your account"))
|
|> put_flash(:info, dgettext("prompts", "please check your email to verify your account"))
|
||||||
|> redirect(to: Routes.user_session_path(Endpoint, :new))
|
|> redirect(to: ~p"/users/log_in")
|
||||||
|
|
||||||
{:error, :invalid_token} ->
|
{:error, :invalid_token} ->
|
||||||
conn
|
conn
|
||||||
|> put_flash(:error, dgettext("errors", "sorry, this invite was not found or expired"))
|
|> put_flash(:error, dgettext("errors", "sorry, this invite was not found or expired"))
|
||||||
|> redirect(to: Routes.live_path(Endpoint, HomeLive))
|
|> redirect(to: ~p"/")
|
||||||
|
|
||||||
{:error, %Ecto.Changeset{} = changeset} ->
|
{:error, %Changeset{} = changeset} ->
|
||||||
conn |> render("new.html", changeset: changeset, invite_token: invite_token)
|
conn |> render(:new, changeset: changeset, invite_token: invite_token)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
5
lib/memex_web/controllers/user_registration_html.ex
Normal file
5
lib/memex_web/controllers/user_registration_html.ex
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
defmodule MemexWeb.UserRegistrationHTML do
|
||||||
|
use MemexWeb, :html
|
||||||
|
|
||||||
|
embed_templates "user_registration_html/*"
|
||||||
|
end
|
@ -6,10 +6,10 @@
|
|||||||
<.form
|
<.form
|
||||||
:let={f}
|
:let={f}
|
||||||
for={@changeset}
|
for={@changeset}
|
||||||
action={Routes.user_registration_path(@conn, :create)}
|
action={~p"/users/register"}
|
||||||
class="flex flex-col space-y-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 justify-center items-center"
|
class="flex flex-col space-y-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 justify-center items-center"
|
||||||
>
|
>
|
||||||
<p :if={@changeset.action && not @changeset.valid?()} class="alert alert-danger col-span-3">
|
<p :if={@changeset.action && not @changeset.valid?} class="alert alert-danger col-span-3">
|
||||||
<%= dgettext("errors", "oops, something went wrong! please check the errors below.") %>
|
<%= dgettext("errors", "oops, something went wrong! please check the errors below.") %>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@ -40,10 +40,10 @@
|
|||||||
<hr class="hr" />
|
<hr class="hr" />
|
||||||
|
|
||||||
<div class="flex flex-row justify-center items-center space-x-4">
|
<div class="flex flex-row justify-center items-center space-x-4">
|
||||||
<.link href={Routes.user_session_path(@conn, :new)} class="btn btn-primary">
|
<.link href={~p"/users/log_in"} class="btn btn-primary">
|
||||||
<%= dgettext("actions", "log in") %>
|
<%= dgettext("actions", "log in") %>
|
||||||
</.link>
|
</.link>
|
||||||
<.link href={Routes.user_reset_password_path(@conn, :new)} class="btn btn-primary">
|
<.link href={~p"/users/reset_password"} class="btn btn-primary">
|
||||||
<%= dgettext("actions", "forgot your password?") %>
|
<%= dgettext("actions", "forgot your password?") %>
|
||||||
</.link>
|
</.link>
|
||||||
</div>
|
</div>
|
@ -6,14 +6,14 @@ defmodule MemexWeb.UserResetPasswordController do
|
|||||||
plug :get_user_by_reset_password_token when action in [:edit, :update]
|
plug :get_user_by_reset_password_token when action in [:edit, :update]
|
||||||
|
|
||||||
def new(conn, _params) do
|
def new(conn, _params) do
|
||||||
render(conn, "new.html", page_title: gettext("forgot your password?"))
|
render(conn, :new, page_title: gettext("forgot your password?"))
|
||||||
end
|
end
|
||||||
|
|
||||||
def create(conn, %{"user" => %{"email" => email}}) do
|
def create(conn, %{"user" => %{"email" => email}}) do
|
||||||
if user = Accounts.get_user_by_email(email) do
|
if user = Accounts.get_user_by_email(email) do
|
||||||
Accounts.deliver_user_reset_password_instructions(
|
Accounts.deliver_user_reset_password_instructions(
|
||||||
user,
|
user,
|
||||||
&Routes.user_reset_password_url(conn, :edit, &1)
|
fn token -> url(MemexWeb.Endpoint, ~p"/users/reset_password/#{token}") end
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -23,17 +23,16 @@ defmodule MemexWeb.UserResetPasswordController do
|
|||||||
:info,
|
:info,
|
||||||
dgettext(
|
dgettext(
|
||||||
"prompts",
|
"prompts",
|
||||||
"If your email is in our system, you will receive instructions to " <>
|
"if your email is in our system, you will receive instructions to reset your password shortly."
|
||||||
"reset your password shortly."
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|> redirect(to: "/")
|
|> redirect(to: ~p"/")
|
||||||
end
|
end
|
||||||
|
|
||||||
def edit(conn, _params) do
|
def edit(conn, _params) do
|
||||||
render(conn, "edit.html",
|
render(conn, :edit,
|
||||||
changeset: Accounts.change_user_password(conn.assigns.user),
|
changeset: Accounts.change_user_password(conn.assigns.user),
|
||||||
page_title: gettext("Reset your password")
|
page_title: gettext("reset your password")
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -41,13 +40,13 @@ defmodule MemexWeb.UserResetPasswordController do
|
|||||||
# leaked token giving the user access to the account.
|
# leaked token giving the user access to the account.
|
||||||
def update(conn, %{"user" => user_params}) do
|
def update(conn, %{"user" => user_params}) do
|
||||||
case Accounts.reset_user_password(conn.assigns.user, user_params) do
|
case Accounts.reset_user_password(conn.assigns.user, user_params) do
|
||||||
{:ok, _} ->
|
{:ok, _user} ->
|
||||||
conn
|
conn
|
||||||
|> put_flash(:info, dgettext("prompts", "Password reset successfully."))
|
|> put_flash(:info, dgettext("prompts", "password reset successfully."))
|
||||||
|> redirect(to: Routes.user_session_path(conn, :new))
|
|> redirect(to: ~p"/users/log_in")
|
||||||
|
|
||||||
{:error, changeset} ->
|
{:error, changeset} ->
|
||||||
render(conn, "edit.html", changeset: changeset)
|
render(conn, :edit, changeset: changeset)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -55,14 +54,14 @@ defmodule MemexWeb.UserResetPasswordController do
|
|||||||
%{"token" => token} = conn.params
|
%{"token" => token} = conn.params
|
||||||
|
|
||||||
if user = Accounts.get_user_by_reset_password_token(token) do
|
if user = Accounts.get_user_by_reset_password_token(token) do
|
||||||
conn |> assign(:user, user) |> assign(:token, token)
|
conn |> assign(user: user, token: token)
|
||||||
else
|
else
|
||||||
conn
|
conn
|
||||||
|> put_flash(
|
|> put_flash(
|
||||||
:error,
|
:error,
|
||||||
dgettext("errors", "Reset password link is invalid or it has expired.")
|
dgettext("errors", "reset password link is invalid or it has expired.")
|
||||||
)
|
)
|
||||||
|> redirect(to: "/")
|
|> redirect(to: ~p"/")
|
||||||
|> halt()
|
|> halt()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
6
lib/memex_web/controllers/user_reset_password_html.ex
Normal file
6
lib/memex_web/controllers/user_reset_password_html.ex
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
defmodule MemexWeb.UserResetPasswordHTML do
|
||||||
|
use MemexWeb, :html
|
||||||
|
alias Memex.Accounts
|
||||||
|
|
||||||
|
embed_templates "user_reset_password_html/*"
|
||||||
|
end
|
@ -1,15 +1,15 @@
|
|||||||
<div class="mx-auto pb-8 max-w-2xl flex flex-col justify-center items-center space-y-4">
|
<div class="mx-auto pb-8 max-w-2xl flex flex-col justify-center items-center space-y-4">
|
||||||
<h1 class="title text-primary-400 text-xl">
|
<h1 class="title text-primary-400 text-xl">
|
||||||
<%= dgettext("actions", "Reset password") %>
|
<%= dgettext("actions", "reset password") %>
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<.form
|
<.form
|
||||||
:let={f}
|
:let={f}
|
||||||
for={@changeset}
|
for={@changeset}
|
||||||
action={Routes.user_reset_password_path(@conn, :update, @token)}
|
action={~p"/users/reset_password/#{@token}"}
|
||||||
class="flex flex-col space-y-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 justify-center items-center"
|
class="flex flex-col space-y-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 justify-center items-center"
|
||||||
>
|
>
|
||||||
<p :if={@changeset.action && not @changeset.valid?()} class="alert alert-danger col-span-3">
|
<p :if={@changeset.action && not @changeset.valid?} class="alert alert-danger col-span-3">
|
||||||
<%= dgettext("errors", "oops, something went wrong! please check the errors below.") %>
|
<%= dgettext("errors", "oops, something went wrong! please check the errors below.") %>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@ -26,7 +26,7 @@
|
|||||||
) %>
|
) %>
|
||||||
<%= error_tag(f, :password_confirmation, "col-span-3") %>
|
<%= error_tag(f, :password_confirmation, "col-span-3") %>
|
||||||
|
|
||||||
<%= submit(dgettext("actions", "Reset password"),
|
<%= submit(dgettext("actions", "reset password"),
|
||||||
class: "mx-auto btn btn-primary col-span-3"
|
class: "mx-auto btn btn-primary col-span-3"
|
||||||
) %>
|
) %>
|
||||||
</.form>
|
</.form>
|
||||||
@ -34,14 +34,10 @@
|
|||||||
<hr class="hr" />
|
<hr class="hr" />
|
||||||
|
|
||||||
<div class="flex flex-row justify-center items-center space-x-4">
|
<div class="flex flex-row justify-center items-center space-x-4">
|
||||||
<.link
|
<.link :if={Accounts.allow_registration?()} href={~p"/users/register"} class="btn btn-primary">
|
||||||
:if={Accounts.allow_registration?()}
|
|
||||||
href={Routes.user_registration_path(@conn, :new)}
|
|
||||||
class="btn btn-primary"
|
|
||||||
>
|
|
||||||
<%= dgettext("actions", "register") %>
|
<%= dgettext("actions", "register") %>
|
||||||
</.link>
|
</.link>
|
||||||
<.link href={Routes.user_session_path(@conn, :new)} class="btn btn-primary">
|
<.link href={~p"/users/log_in"} class="btn btn-primary">
|
||||||
<%= dgettext("actions", "log in") %>
|
<%= dgettext("actions", "log in") %>
|
||||||
</.link>
|
</.link>
|
||||||
</div>
|
</div>
|
@ -7,7 +7,7 @@
|
|||||||
:let={f}
|
:let={f}
|
||||||
for={%{}}
|
for={%{}}
|
||||||
as={:user}
|
as={:user}
|
||||||
action={Routes.user_reset_password_path(@conn, :create)}
|
action={~p"/users/reset_password"}
|
||||||
class="flex flex-col space-y-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 justify-center items-center"
|
class="flex flex-col space-y-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 justify-center items-center"
|
||||||
>
|
>
|
||||||
<%= label(f, :email, gettext("email"), class: "title text-lg text-primary-400") %>
|
<%= label(f, :email, gettext("email"), class: "title text-lg text-primary-400") %>
|
||||||
@ -21,14 +21,10 @@
|
|||||||
<hr class="hr" />
|
<hr class="hr" />
|
||||||
|
|
||||||
<div class="flex flex-row justify-center items-center space-x-4">
|
<div class="flex flex-row justify-center items-center space-x-4">
|
||||||
<.link
|
<.link :if={Accounts.allow_registration?()} href={~p"/users/register"} class="btn btn-primary">
|
||||||
:if={Accounts.allow_registration?()}
|
|
||||||
href={Routes.user_registration_path(@conn, :new)}
|
|
||||||
class="btn btn-primary"
|
|
||||||
>
|
|
||||||
<%= dgettext("actions", "register") %>
|
<%= dgettext("actions", "register") %>
|
||||||
</.link>
|
</.link>
|
||||||
<.link href={Routes.user_session_path(@conn, :new)} class="btn btn-primary">
|
<.link href={~p"/users/log_in"} class="btn btn-primary">
|
||||||
<%= dgettext("actions", "log in") %>
|
<%= dgettext("actions", "log in") %>
|
||||||
</.link>
|
</.link>
|
||||||
</div>
|
</div>
|
@ -5,7 +5,7 @@ defmodule MemexWeb.UserSessionController do
|
|||||||
alias MemexWeb.UserAuth
|
alias MemexWeb.UserAuth
|
||||||
|
|
||||||
def new(conn, _params) do
|
def new(conn, _params) do
|
||||||
render(conn, "new.html", error_message: nil, page_title: gettext("log in"))
|
render(conn, :new, error_message: nil, page_title: gettext("log in"))
|
||||||
end
|
end
|
||||||
|
|
||||||
def create(conn, %{"user" => user_params}) do
|
def create(conn, %{"user" => user_params}) do
|
||||||
@ -14,7 +14,7 @@ defmodule MemexWeb.UserSessionController do
|
|||||||
if user = Accounts.get_user_by_email_and_password(email, password) do
|
if user = Accounts.get_user_by_email_and_password(email, password) do
|
||||||
UserAuth.log_in_user(conn, user, user_params)
|
UserAuth.log_in_user(conn, user, user_params)
|
||||||
else
|
else
|
||||||
render(conn, "new.html", error_message: dgettext("errors", "Invalid email or password"))
|
render(conn, :new, error_message: dgettext("errors", "invalid email or password"))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
6
lib/memex_web/controllers/user_session_html.ex
Normal file
6
lib/memex_web/controllers/user_session_html.ex
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
defmodule MemexWeb.UserSessionHTML do
|
||||||
|
use MemexWeb, :html
|
||||||
|
alias Memex.Accounts
|
||||||
|
|
||||||
|
embed_templates "user_session_html/*"
|
||||||
|
end
|
@ -6,8 +6,8 @@
|
|||||||
<.form
|
<.form
|
||||||
:let={f}
|
:let={f}
|
||||||
for={@conn}
|
for={@conn}
|
||||||
action={Routes.user_session_path(@conn, :create)}
|
action={~p"/users/log_in"}
|
||||||
as="user"
|
as={:user}
|
||||||
class="flex flex-col space-y-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 justify-center items-center"
|
class="flex flex-col space-y-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 justify-center items-center"
|
||||||
>
|
>
|
||||||
<p :if={@error_message} class="alert alert-danger col-span-3">
|
<p :if={@error_message} class="alert alert-danger col-span-3">
|
||||||
@ -31,14 +31,10 @@
|
|||||||
<hr class="hr" />
|
<hr class="hr" />
|
||||||
|
|
||||||
<div class="flex flex-row justify-center items-center space-x-4">
|
<div class="flex flex-row justify-center items-center space-x-4">
|
||||||
<.link
|
<.link :if={Accounts.allow_registration?()} href={~p"/users/register"} class="btn btn-primary">
|
||||||
:if={Accounts.allow_registration?()}
|
|
||||||
href={Routes.user_registration_path(@conn, :new)}
|
|
||||||
class="btn btn-primary"
|
|
||||||
>
|
|
||||||
<%= dgettext("actions", "register") %>
|
<%= dgettext("actions", "register") %>
|
||||||
</.link>
|
</.link>
|
||||||
<.link href={Routes.user_reset_password_path(@conn, :new)} class="btn btn-primary">
|
<.link href={~p"/users/reset_password"} class="btn btn-primary">
|
||||||
<%= dgettext("actions", "forgot your password?") %>
|
<%= dgettext("actions", "forgot your password?") %>
|
||||||
</.link>
|
</.link>
|
||||||
</div>
|
</div>
|
@ -1,13 +1,13 @@
|
|||||||
defmodule MemexWeb.UserSettingsController do
|
defmodule MemexWeb.UserSettingsController do
|
||||||
use MemexWeb, :controller
|
use MemexWeb, :controller
|
||||||
import MemexWeb.Gettext
|
use Gettext, backend: MemexWeb.Gettext
|
||||||
alias Memex.Accounts
|
alias Memex.Accounts
|
||||||
alias MemexWeb.{HomeLive, UserAuth}
|
alias MemexWeb.UserAuth
|
||||||
|
|
||||||
plug :assign_email_and_password_changesets
|
plug :assign_email_and_password_changesets
|
||||||
|
|
||||||
def edit(conn, _params) do
|
def edit(conn, _params) do
|
||||||
render(conn, "edit.html", page_title: gettext("settings"))
|
render(conn, :edit, page_title: gettext("settings"))
|
||||||
end
|
end
|
||||||
|
|
||||||
def update(%{assigns: %{current_user: user}} = conn, %{
|
def update(%{assigns: %{current_user: user}} = conn, %{
|
||||||
@ -20,7 +20,7 @@ defmodule MemexWeb.UserSettingsController do
|
|||||||
Accounts.deliver_update_email_instructions(
|
Accounts.deliver_update_email_instructions(
|
||||||
applied_user,
|
applied_user,
|
||||||
user.email,
|
user.email,
|
||||||
&Routes.user_settings_url(conn, :confirm_email, &1)
|
fn token -> url(MemexWeb.Endpoint, ~p"/users/settings/confirm_email/#{token}") end
|
||||||
)
|
)
|
||||||
|
|
||||||
conn
|
conn
|
||||||
@ -31,10 +31,10 @@ defmodule MemexWeb.UserSettingsController do
|
|||||||
"a link to confirm your email change has been sent to the new address."
|
"a link to confirm your email change has been sent to the new address."
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|> redirect(to: Routes.user_settings_path(conn, :edit))
|
|> redirect(to: ~p"/users/settings")
|
||||||
|
|
||||||
{:error, changeset} ->
|
{:error, changeset} ->
|
||||||
conn |> render("edit.html", email_changeset: changeset)
|
conn |> render(:edit, email_changeset: changeset)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -47,11 +47,11 @@ defmodule MemexWeb.UserSettingsController do
|
|||||||
{:ok, user} ->
|
{:ok, user} ->
|
||||||
conn
|
conn
|
||||||
|> put_flash(:info, dgettext("prompts", "password updated successfully."))
|
|> put_flash(:info, dgettext("prompts", "password updated successfully."))
|
||||||
|> put_session(:user_return_to, Routes.user_settings_path(conn, :edit))
|
|> put_session(:user_return_to, ~p"/users/settings")
|
||||||
|> UserAuth.log_in_user(user)
|
|> UserAuth.log_in_user(user)
|
||||||
|
|
||||||
{:error, changeset} ->
|
{:error, changeset} ->
|
||||||
conn |> render("edit.html", password_changeset: changeset)
|
conn |> render(:edit, password_changeset: changeset)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -63,10 +63,10 @@ defmodule MemexWeb.UserSettingsController do
|
|||||||
{:ok, _user} ->
|
{:ok, _user} ->
|
||||||
conn
|
conn
|
||||||
|> put_flash(:info, dgettext("prompts", "language updated successfully."))
|
|> put_flash(:info, dgettext("prompts", "language updated successfully."))
|
||||||
|> redirect(to: Routes.user_settings_path(conn, :edit))
|
|> redirect(to: ~p"/users/settings")
|
||||||
|
|
||||||
{:error, changeset} ->
|
{:error, changeset} ->
|
||||||
conn |> render("edit.html", locale_changeset: changeset)
|
conn |> render(:edit, locale_changeset: changeset)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -75,7 +75,7 @@ defmodule MemexWeb.UserSettingsController do
|
|||||||
:ok ->
|
:ok ->
|
||||||
conn
|
conn
|
||||||
|> put_flash(:info, dgettext("prompts", "email changed successfully."))
|
|> put_flash(:info, dgettext("prompts", "email changed successfully."))
|
||||||
|> redirect(to: Routes.user_settings_path(conn, :edit))
|
|> redirect(to: ~p"/users/settings")
|
||||||
|
|
||||||
:error ->
|
:error ->
|
||||||
conn
|
conn
|
||||||
@ -83,7 +83,7 @@ defmodule MemexWeb.UserSettingsController do
|
|||||||
:error,
|
:error,
|
||||||
dgettext("errors", "email change link is invalid or it has expired.")
|
dgettext("errors", "email change link is invalid or it has expired.")
|
||||||
)
|
)
|
||||||
|> redirect(to: Routes.user_settings_path(conn, :edit))
|
|> redirect(to: ~p"/users/settings")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -93,18 +93,20 @@ defmodule MemexWeb.UserSettingsController do
|
|||||||
|
|
||||||
conn
|
conn
|
||||||
|> put_flash(:error, dgettext("prompts", "your account has been deleted"))
|
|> put_flash(:error, dgettext("prompts", "your account has been deleted"))
|
||||||
|> redirect(to: Routes.live_path(conn, HomeLive))
|
|> redirect(to: ~p"/")
|
||||||
else
|
else
|
||||||
conn
|
conn
|
||||||
|> put_flash(:error, dgettext("errors", "unable to delete user"))
|
|> put_flash(:error, dgettext("errors", "unable to delete user"))
|
||||||
|> redirect(to: Routes.user_settings_path(conn, :edit))
|
|> redirect(to: ~p"/users/settings")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp assign_email_and_password_changesets(%{assigns: %{current_user: user}} = conn, _opts) do
|
defp assign_email_and_password_changesets(%{assigns: %{current_user: user}} = conn, _opts) do
|
||||||
conn
|
conn
|
||||||
|> assign(:email_changeset, Accounts.change_user_email(user))
|
|> assign(
|
||||||
|> assign(:password_changeset, Accounts.change_user_password(user))
|
email_changeset: Accounts.change_user_email(user),
|
||||||
|> assign(:locale_changeset, Accounts.change_user_locale(user))
|
locale_changeset: Accounts.change_user_locale(user),
|
||||||
|
password_changeset: Accounts.change_user_password(user)
|
||||||
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
5
lib/memex_web/controllers/user_settings_html.ex
Normal file
5
lib/memex_web/controllers/user_settings_html.ex
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
defmodule MemexWeb.UserSettingsHTML do
|
||||||
|
use MemexWeb, :html
|
||||||
|
|
||||||
|
embed_templates "user_settings_html/*"
|
||||||
|
end
|
@ -8,7 +8,7 @@
|
|||||||
<.form
|
<.form
|
||||||
:let={f}
|
:let={f}
|
||||||
for={@email_changeset}
|
for={@email_changeset}
|
||||||
action={Routes.user_settings_path(@conn, :update)}
|
action={~p"/users/settings"}
|
||||||
class="flex flex-col space-y-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 justify-center items-center"
|
class="flex flex-col space-y-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 justify-center items-center"
|
||||||
>
|
>
|
||||||
<h3 class="title text-primary-400 text-lg text-center col-span-3">
|
<h3 class="title text-primary-400 text-lg text-center col-span-3">
|
||||||
@ -16,7 +16,7 @@
|
|||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
:if={@email_changeset.action && not @email_changeset.valid?()}
|
:if={@email_changeset.action && not @email_changeset.valid?}
|
||||||
class="alert alert-danger col-span-3"
|
class="alert alert-danger col-span-3"
|
||||||
>
|
>
|
||||||
<%= dgettext("errors", "oops, something went wrong! please check the errors below") %>
|
<%= dgettext("errors", "oops, something went wrong! please check the errors below") %>
|
||||||
@ -50,7 +50,7 @@
|
|||||||
<.form
|
<.form
|
||||||
:let={f}
|
:let={f}
|
||||||
for={@password_changeset}
|
for={@password_changeset}
|
||||||
action={Routes.user_settings_path(@conn, :update)}
|
action={~p"/users/settings"}
|
||||||
class="flex flex-col space-y-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 justify-center items-center"
|
class="flex flex-col space-y-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 justify-center items-center"
|
||||||
>
|
>
|
||||||
<h3 class="title text-primary-400 text-lg text-center col-span-3">
|
<h3 class="title text-primary-400 text-lg text-center col-span-3">
|
||||||
@ -58,7 +58,7 @@
|
|||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<p
|
<p
|
||||||
:if={@password_changeset.action && not @password_changeset.valid?()}
|
:if={@password_changeset.action && not @password_changeset.valid?}
|
||||||
class="alert alert-danger col-span-3"
|
class="alert alert-danger col-span-3"
|
||||||
>
|
>
|
||||||
<%= dgettext("errors", "oops, something went wrong! please check the errors below.") %>
|
<%= dgettext("errors", "oops, something went wrong! please check the errors below.") %>
|
||||||
@ -104,7 +104,7 @@
|
|||||||
<.form
|
<.form
|
||||||
:let={f}
|
:let={f}
|
||||||
for={@locale_changeset}
|
for={@locale_changeset}
|
||||||
action={Routes.user_settings_path(@conn, :update)}
|
action={~p"/users/settings"}
|
||||||
class="flex flex-col space-y-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 justify-center items-center"
|
class="flex flex-col space-y-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 justify-center items-center"
|
||||||
>
|
>
|
||||||
<%= label(f, :locale, dgettext("actions", "change language"),
|
<%= label(f, :locale, dgettext("actions", "change language"),
|
||||||
@ -112,7 +112,7 @@
|
|||||||
) %>
|
) %>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
:if={@locale_changeset.action && not @locale_changeset.valid?()}
|
:if={@locale_changeset.action && not @locale_changeset.valid?}
|
||||||
class="alert alert-danger col-span-3"
|
class="alert alert-danger col-span-3"
|
||||||
>
|
>
|
||||||
<%= dgettext("errors", "oops, something went wrong! please check the errors below") %>
|
<%= dgettext("errors", "oops, something went wrong! please check the errors below") %>
|
||||||
@ -134,17 +134,13 @@
|
|||||||
<hr class="hr" />
|
<hr class="hr" />
|
||||||
|
|
||||||
<div class="flex justify-end items-center">
|
<div class="flex justify-end items-center">
|
||||||
<.link
|
<.link href={~p"/export/json"} class="mx-4 my-2 btn btn-primary" target="_blank">
|
||||||
href={Routes.export_path(@conn, :export, :json)}
|
|
||||||
class="mx-4 my-2 btn btn-primary"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
<%= dgettext("actions", "export data as json") %>
|
<%= dgettext("actions", "export data as json") %>
|
||||||
</.link>
|
</.link>
|
||||||
|
|
||||||
<.link
|
<.link
|
||||||
href={Routes.user_settings_path(@conn, :delete, @current_user)}
|
href={~p"/users/settings/#{@current_user}"}
|
||||||
method={:delete}
|
method="delete"
|
||||||
class="mx-4 my-2 btn btn-alert"
|
class="mx-4 my-2 btn btn-alert"
|
||||||
data-confirm={dgettext("prompts", "are you sure you want to delete your account?")}
|
data-confirm={dgettext("prompts", "are you sure you want to delete your account?")}
|
||||||
>
|
>
|
@ -20,7 +20,7 @@ defmodule MemexWeb.Endpoint do
|
|||||||
at: "/",
|
at: "/",
|
||||||
from: :memex,
|
from: :memex,
|
||||||
gzip: false,
|
gzip: false,
|
||||||
only: ~w(css fonts images js favicon.ico robots.txt)
|
only: MemexWeb.static_paths()
|
||||||
|
|
||||||
# Code reloading can be explicitly enabled under the
|
# Code reloading can be explicitly enabled under the
|
||||||
# :code_reloader configuration of your endpoint.
|
# :code_reloader configuration of your endpoint.
|
||||||
|
@ -3,8 +3,8 @@ defmodule MemexWeb.ErrorHelpers do
|
|||||||
Conveniences for translating and building error messages.
|
Conveniences for translating and building error messages.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
use Phoenix.HTML
|
use PhoenixHTMLHelpers
|
||||||
import Phoenix.Component
|
import Phoenix.{Component, HTML.Form}
|
||||||
alias Ecto.Changeset
|
alias Ecto.Changeset
|
||||||
alias Phoenix.{HTML.Form, LiveView.Rendered}
|
alias Phoenix.{HTML.Form, LiveView.Rendered}
|
||||||
|
|
@ -5,7 +5,7 @@ defmodule MemexWeb.Gettext do
|
|||||||
By using [Gettext](https://hexdocs.pm/gettext),
|
By using [Gettext](https://hexdocs.pm/gettext),
|
||||||
your module gains a set of macros for translations, for example:
|
your module gains a set of macros for translations, for example:
|
||||||
|
|
||||||
import MemexWeb.Gettext
|
use Gettext, backend: MemexWeb.Gettext
|
||||||
|
|
||||||
# Simple translation
|
# Simple translation
|
||||||
gettext("Here is the string to translate")
|
gettext("Here is the string to translate")
|
||||||
@ -20,5 +20,5 @@ defmodule MemexWeb.Gettext do
|
|||||||
|
|
||||||
See the [Gettext Docs](https://hexdocs.pm/gettext) for detailed usage.
|
See the [Gettext Docs](https://hexdocs.pm/gettext) for detailed usage.
|
||||||
"""
|
"""
|
||||||
use Gettext, otp_app: :memex
|
use Gettext.Backend, otp_app: :memex
|
||||||
end
|
end
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
defmodule MemexWeb.ViewHelpers do
|
defmodule MemexWeb.HTMLHelpers do
|
||||||
@moduledoc """
|
@moduledoc """
|
||||||
Contains common helpers that can be used in liveviews and regular views. These
|
Contains common helpers that are used for rendering
|
||||||
are automatically imported into any Phoenix View using `use MemexWeb,
|
|
||||||
:view`
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
use Phoenix.Component
|
use Phoenix.Component
|
@ -69,7 +69,7 @@ defmodule MemexWeb.ContextLive.FormComponent do
|
|||||||
|> push_navigate(to: return_to)
|
|> push_navigate(to: return_to)
|
||||||
|
|
||||||
{:error, %Changeset{} = changeset} ->
|
{:error, %Changeset{} = changeset} ->
|
||||||
assign(socket, changeset: changeset)
|
assign(socket, :changeset, changeset)
|
||||||
end
|
end
|
||||||
|
|
||||||
{:noreply, socket}
|
{:noreply, socket}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
<div class="h-full flex flex-col justify-start items-stretch space-y-4">
|
<div class="flex flex-col justify-start items-stretch space-y-4 h-full">
|
||||||
<.form
|
<.form
|
||||||
:let={f}
|
:let={f}
|
||||||
for={@changeset}
|
for={@changeset}
|
||||||
@ -6,31 +6,38 @@
|
|||||||
phx-target={@myself}
|
phx-target={@myself}
|
||||||
phx-change="validate"
|
phx-change="validate"
|
||||||
phx-submit="save"
|
phx-submit="save"
|
||||||
phx-debounce="300"
|
phx-hook="CtrlEnter"
|
||||||
class="flex flex-col justify-start items-stretch space-y-4"
|
class="flex flex-col justify-start items-stretch space-y-4"
|
||||||
>
|
>
|
||||||
<%= text_input(f, :slug,
|
<%= text_input(f, :slug,
|
||||||
|
aria_label: gettext("slug"),
|
||||||
class: "input input-primary",
|
class: "input input-primary",
|
||||||
|
phx_debounce: 300,
|
||||||
|
phx_hook: "SanitizeTitles",
|
||||||
placeholder: gettext("slug"),
|
placeholder: gettext("slug"),
|
||||||
aria_label: gettext("slug")
|
required: true
|
||||||
) %>
|
) %>
|
||||||
<%= error_tag(f, :slug) %>
|
<%= error_tag(f, :slug) %>
|
||||||
|
|
||||||
<%= textarea(f, :content,
|
<%= textarea(f, :content,
|
||||||
id: "context-form-content",
|
id: "context-form-content",
|
||||||
class: "input input-primary h-64 min-h-64",
|
class: "input input-primary h-64 min-h-64",
|
||||||
phx_hook: "MaintainAttrs",
|
|
||||||
phx_update: "ignore",
|
phx_update: "ignore",
|
||||||
placeholder: gettext("use [[note-slug]] to link to a note"),
|
placeholder:
|
||||||
aria_label: gettext("use [[note-slug]] to link to a note")
|
gettext("use [[note-slug]] to link to a note or [context-slug] to link to a context"),
|
||||||
|
aria_label:
|
||||||
|
gettext("use [[note-slug]] to link to a note or [context-slug] to link to a context"),
|
||||||
|
phx_debounce: 300
|
||||||
) %>
|
) %>
|
||||||
<%= error_tag(f, :content) %>
|
<%= error_tag(f, :content) %>
|
||||||
|
|
||||||
<%= text_input(f, :tags_string,
|
<%= text_input(f, :tags_string,
|
||||||
id: "tags-input",
|
aria_label: gettext("tag1,tag2"),
|
||||||
class: "input input-primary",
|
class: "input input-primary",
|
||||||
placeholder: gettext("tag1,tag2"),
|
id: "tags-input",
|
||||||
aria_label: gettext("tag1,tag2")
|
phx_debounce: 300,
|
||||||
|
phx_hook: "SanitizeTags",
|
||||||
|
placeholder: gettext("tag1,tag2")
|
||||||
) %>
|
) %>
|
||||||
<%= error_tag(f, :tags_string) %>
|
<%= error_tag(f, :tags_string) %>
|
||||||
|
|
||||||
@ -38,7 +45,8 @@
|
|||||||
<%= select(f, :visibility, Ecto.Enum.values(Memex.Contexts.Context, :visibility),
|
<%= select(f, :visibility, Ecto.Enum.values(Memex.Contexts.Context, :visibility),
|
||||||
class: "grow input input-primary",
|
class: "grow input input-primary",
|
||||||
prompt: gettext("select privacy"),
|
prompt: gettext("select privacy"),
|
||||||
aria_label: gettext("select privacy")
|
aria_label: gettext("select privacy"),
|
||||||
|
phx_debounce: 300
|
||||||
) %>
|
) %>
|
||||||
|
|
||||||
<%= submit(dgettext("actions", "save"),
|
<%= submit(dgettext("actions", "save"),
|
||||||
|
@ -20,29 +20,37 @@ defmodule MemexWeb.ContextLive.Index do
|
|||||||
%{slug: slug} = context = Contexts.get_context_by_slug(slug, current_user)
|
%{slug: slug} = context = Contexts.get_context_by_slug(slug, current_user)
|
||||||
|
|
||||||
socket
|
socket
|
||||||
|> assign(page_title: gettext("edit %{slug}", slug: slug))
|
|> assign(
|
||||||
|> assign(context: context)
|
context: context,
|
||||||
|
page_title: gettext("edit %{slug}", slug: slug)
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp apply_action(%{assigns: %{current_user: %{id: current_user_id}}} = socket, :new, _params) do
|
defp apply_action(%{assigns: %{current_user: %{id: current_user_id}}} = socket, :new, _params) do
|
||||||
socket
|
socket
|
||||||
|> assign(page_title: gettext("new context"))
|
|> assign(
|
||||||
|> assign(context: %Context{visibility: :private, user_id: current_user_id})
|
context: %Context{visibility: :private, user_id: current_user_id},
|
||||||
|
page_title: gettext("new context")
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp apply_action(socket, :index, _params) do
|
defp apply_action(socket, :index, _params) do
|
||||||
socket
|
socket
|
||||||
|> assign(page_title: gettext("contexts"))
|
|> assign(
|
||||||
|> assign(search: nil)
|
context: nil,
|
||||||
|> assign(context: nil)
|
page_title: gettext("contexts"),
|
||||||
|
search: nil
|
||||||
|
)
|
||||||
|> display_contexts()
|
|> display_contexts()
|
||||||
end
|
end
|
||||||
|
|
||||||
defp apply_action(socket, :search, %{"search" => search}) do
|
defp apply_action(socket, :search, %{"search" => search}) do
|
||||||
socket
|
socket
|
||||||
|> assign(page_title: gettext("contexts"))
|
|> assign(
|
||||||
|> assign(search: search)
|
context: nil,
|
||||||
|> assign(context: nil)
|
page_title: gettext("contexts"),
|
||||||
|
search: search
|
||||||
|
)
|
||||||
|> display_contexts()
|
|> display_contexts()
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -60,16 +68,15 @@ defmodule MemexWeb.ContextLive.Index do
|
|||||||
end
|
end
|
||||||
|
|
||||||
def handle_event("search", %{"search" => %{"search_term" => ""}}, socket) do
|
def handle_event("search", %{"search" => %{"search_term" => ""}}, socket) do
|
||||||
{:noreply, socket |> push_patch(to: Routes.context_index_path(Endpoint, :index))}
|
{:noreply, socket |> push_patch(to: ~p"/contexts")}
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_event("search", %{"search" => %{"search_term" => search_term}}, socket) do
|
def handle_event("search", %{"search" => %{"search_term" => search_term}}, socket) do
|
||||||
redirect_to = Routes.context_index_path(Endpoint, :search, search_term)
|
redirect_to = ~p"/contexts/#{search_term}"
|
||||||
{:noreply, socket |> push_patch(to: redirect_to)}
|
{:noreply, socket |> push_patch(to: redirect_to)}
|
||||||
end
|
end
|
||||||
|
|
||||||
defp display_contexts(%{assigns: %{current_user: current_user, search: search}} = socket)
|
defp display_contexts(%{assigns: %{current_user: %{} = current_user, search: search}} = socket) do
|
||||||
when not (current_user |> is_nil()) do
|
|
||||||
socket |> assign(contexts: Contexts.list_contexts(search, current_user))
|
socket |> assign(contexts: Contexts.list_contexts(search, current_user))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
<div class="mx-auto flex flex-col justify-center items-start space-y-4 max-w-3xl">
|
<div class="flex flex-col justify-center items-start mx-auto space-y-4 max-w-3xl">
|
||||||
<h1 class="text-xl">
|
<h1 class="text-xl">
|
||||||
<%= gettext("contexts") %>
|
<%= gettext("contexts") %>
|
||||||
</h1>
|
</h1>
|
||||||
@ -9,7 +9,7 @@
|
|||||||
as={:search}
|
as={:search}
|
||||||
phx-change="search"
|
phx-change="search"
|
||||||
phx-submit="search"
|
phx-submit="search"
|
||||||
class="self-stretch flex flex-col items-stretch"
|
class="flex flex-col items-stretch self-stretch"
|
||||||
>
|
>
|
||||||
<%= text_input(f, :search_term,
|
<%= text_input(f, :search_term,
|
||||||
class: "input input-primary",
|
class: "input input-primary",
|
||||||
@ -33,14 +33,14 @@
|
|||||||
>
|
>
|
||||||
<:actions :let={context}>
|
<:actions :let={context}>
|
||||||
<.link
|
<.link
|
||||||
:if={Contexts.is_owner?(context, @current_user)}
|
:if={@current_user}
|
||||||
patch={Routes.context_index_path(@socket, :edit, context.slug)}
|
patch={~p"/contexts/#{context}/edit"}
|
||||||
aria-label={dgettext("actions", "edit %{context_slug}", context_slug: context.slug)}
|
aria-label={dgettext("actions", "edit %{context_slug}", context_slug: context.slug)}
|
||||||
>
|
>
|
||||||
<%= dgettext("actions", "edit") %>
|
<%= dgettext("actions", "edit") %>
|
||||||
</.link>
|
</.link>
|
||||||
<.link
|
<.link
|
||||||
:if={Contexts.is_owner_or_admin?(context, @current_user)}
|
:if={@current_user}
|
||||||
href="#"
|
href="#"
|
||||||
phx-click="delete"
|
phx-click="delete"
|
||||||
phx-value-id={context.id}
|
phx-value-id={context.id}
|
||||||
@ -53,16 +53,12 @@
|
|||||||
</.live_component>
|
</.live_component>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<.link
|
<.link :if={@current_user} patch={~p"/contexts/new"} class="self-end btn btn-primary">
|
||||||
:if={@current_user}
|
|
||||||
patch={Routes.context_index_path(@socket, :new)}
|
|
||||||
class="self-end btn btn-primary"
|
|
||||||
>
|
|
||||||
<%= dgettext("actions", "new context") %>
|
<%= dgettext("actions", "new context") %>
|
||||||
</.link>
|
</.link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<.modal :if={@live_action in [:new, :edit]} return_to={Routes.context_index_path(@socket, :index)}>
|
<.modal :if={@live_action in [:new, :edit]} return_to={~p"/contexts"}>
|
||||||
<.live_component
|
<.live_component
|
||||||
module={MemexWeb.ContextLive.FormComponent}
|
module={MemexWeb.ContextLive.FormComponent}
|
||||||
id={@context.id || :new}
|
id={@context.id || :new}
|
||||||
@ -70,6 +66,6 @@
|
|||||||
title={@page_title}
|
title={@page_title}
|
||||||
action={@live_action}
|
action={@live_action}
|
||||||
context={@context}
|
context={@context}
|
||||||
return_to={Routes.context_index_path(@socket, :index)}
|
return_to={~p"/contexts"}
|
||||||
/>
|
/>
|
||||||
</.modal>
|
</.modal>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
defmodule MemexWeb.ContextLive.Show do
|
defmodule MemexWeb.ContextLive.Show do
|
||||||
use MemexWeb, :live_view
|
use MemexWeb, :live_view
|
||||||
alias Memex.Contexts
|
alias Memex.{Contexts, Pipelines}
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def mount(_params, _session, socket) do
|
def mount(_params, _session, socket) do
|
||||||
@ -21,8 +21,12 @@ defmodule MemexWeb.ContextLive.Show do
|
|||||||
|
|
||||||
socket =
|
socket =
|
||||||
socket
|
socket
|
||||||
|> assign(:page_title, page_title(live_action, context))
|
|> assign(
|
||||||
|> assign(:context, context)
|
context: context,
|
||||||
|
page_title: page_title(live_action, context),
|
||||||
|
context_backlinks: Contexts.backlink("[#{context.slug}]", current_user),
|
||||||
|
pipeline_backlinks: Pipelines.backlink("[[#{context.slug}]]", current_user)
|
||||||
|
)
|
||||||
|
|
||||||
{:noreply, socket}
|
{:noreply, socket}
|
||||||
end
|
end
|
||||||
@ -38,7 +42,7 @@ defmodule MemexWeb.ContextLive.Show do
|
|||||||
socket =
|
socket =
|
||||||
socket
|
socket
|
||||||
|> put_flash(:info, gettext("%{slug} deleted", slug: slug))
|
|> put_flash(:info, gettext("%{slug} deleted", slug: slug))
|
||||||
|> push_navigate(to: Routes.context_index_path(Endpoint, :index))
|
|> push_navigate(to: ~p"/contexts")
|
||||||
|
|
||||||
{:noreply, socket}
|
{:noreply, socket}
|
||||||
end
|
end
|
||||||
|
@ -1,34 +1,47 @@
|
|||||||
<div class="mx-auto flex flex-col justify-center items-stretch space-y-4 max-w-3xl">
|
<div class="flex flex-col justify-center items-stretch mx-auto space-y-4 max-w-3xl">
|
||||||
<h1 class="text-xl">
|
<h1 class="text-xl">
|
||||||
<%= @context.slug %>
|
<%= @context.slug %>
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<div class="flex flex-wrap space-x-1">
|
<div class="flex flex-wrap space-x-1">
|
||||||
<.link
|
<.link :for={tag <- @context.tags} navigate={~p"/contexts/#{tag}"} class="link">
|
||||||
:for={tag <- @context.tags}
|
|
||||||
navigate={Routes.context_index_path(Endpoint, :search, tag)}
|
|
||||||
class="link"
|
|
||||||
>
|
|
||||||
<%= tag %>
|
<%= tag %>
|
||||||
</.link>
|
</.link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<.context_content context={@context} />
|
<.context_content context={@context} />
|
||||||
|
|
||||||
|
<div
|
||||||
|
:if={@context_backlinks ++ @pipeline_backlinks != []}
|
||||||
|
class="flex flex-wrap justify-end items-center self-end"
|
||||||
|
>
|
||||||
|
<p><%= gettext("Backlinked by:") %></p>
|
||||||
|
<.link
|
||||||
|
:for={backlink <- @context_backlinks}
|
||||||
|
class="m-1 hover:underline"
|
||||||
|
patch={~p"/context/#{backlink}"}
|
||||||
|
>
|
||||||
|
<%= gettext("[%{slug}]", slug: backlink.slug) %>
|
||||||
|
</.link>
|
||||||
|
<.link
|
||||||
|
:for={backlink <- @pipeline_backlinks}
|
||||||
|
class="m-1 hover:underline"
|
||||||
|
patch={~p"/pipeline/#{backlink}"}
|
||||||
|
>
|
||||||
|
<%= gettext("[[%{slug}]]", slug: backlink.slug) %>
|
||||||
|
</.link>
|
||||||
|
</div>
|
||||||
|
|
||||||
<p class="self-end">
|
<p class="self-end">
|
||||||
<%= gettext("Visibility: %{visibility}", visibility: @context.visibility) %>
|
<%= gettext("Visibility: %{visibility}", visibility: @context.visibility) %>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="self-end flex space-x-4">
|
<div class="flex self-end space-x-4">
|
||||||
<.link
|
<.link :if={@current_user} class="btn btn-primary" patch={~p"/context/#{@context}/edit"}>
|
||||||
:if={Contexts.is_owner?(@context, @current_user)}
|
|
||||||
class="btn btn-primary"
|
|
||||||
patch={Routes.context_show_path(@socket, :edit, @context.slug)}
|
|
||||||
>
|
|
||||||
<%= dgettext("actions", "edit") %>
|
<%= dgettext("actions", "edit") %>
|
||||||
</.link>
|
</.link>
|
||||||
<button
|
<button
|
||||||
:if={Contexts.is_owner_or_admin?(@context, @current_user)}
|
:if={@current_user}
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-primary"
|
class="btn btn-primary"
|
||||||
phx-click="delete"
|
phx-click="delete"
|
||||||
@ -40,10 +53,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<.modal
|
<.modal :if={@live_action == :edit} return_to={~p"/context/#{@context}"}>
|
||||||
:if={@live_action == :edit}
|
|
||||||
return_to={Routes.context_show_path(@socket, :show, @context.slug)}
|
|
||||||
>
|
|
||||||
<.live_component
|
<.live_component
|
||||||
module={MemexWeb.ContextLive.FormComponent}
|
module={MemexWeb.ContextLive.FormComponent}
|
||||||
id={@context.id}
|
id={@context.id}
|
||||||
@ -51,6 +61,6 @@
|
|||||||
title={@page_title}
|
title={@page_title}
|
||||||
action={@live_action}
|
action={@live_action}
|
||||||
context={@context}
|
context={@context}
|
||||||
return_to={Routes.context_show_path(@socket, :show, @context.slug)}
|
return_to={~p"/context/#{@context}"}
|
||||||
/>
|
/>
|
||||||
</.modal>
|
</.modal>
|
||||||
|
@ -89,7 +89,7 @@
|
|||||||
<p>
|
<p>
|
||||||
<%= gettext("in my opinion, contexts should be like single-topic blog posts.") %>
|
<%= gettext("in my opinion, contexts should be like single-topic blog posts.") %>
|
||||||
<%= gettext(
|
<%= gettext(
|
||||||
"for instance, a good context could be what makes some physical designs spark joy for you, and in that context you could backlink to the spoon note as an example of how it fits nicely into your hand."
|
"for instance, a good context could be what makes some physical designs spark joy for you, and in that context you could link to the spoon note as an example of how it fits nicely into your hand."
|
||||||
) %>
|
) %>
|
||||||
</p>
|
</p>
|
||||||
</li>
|
</li>
|
||||||
@ -100,10 +100,10 @@
|
|||||||
</b>
|
</b>
|
||||||
<p>
|
<p>
|
||||||
<%= gettext(
|
<%= gettext(
|
||||||
"in my opinion, pipelines should be pretty lightweight, and just backlink to contexts to provide most of the heavy lifting."
|
"in my opinion, pipelines should be pretty lightweight, and just link to contexts to provide most of the heavy lifting."
|
||||||
) %>
|
) %>
|
||||||
<%= gettext(
|
<%= gettext(
|
||||||
"for instance, a pipeline for buying an object could have a step where you consider how much it sparks joy, and it could backlink to the physical designs context, maybe with some notes about how it applies in this case."
|
"for instance, a pipeline for buying an object could have a step where you consider how much it sparks joy, and it could link to the physical designs context, maybe with some notes about how it applies in this case."
|
||||||
) %>
|
) %>
|
||||||
</p>
|
</p>
|
||||||
</li>
|
</li>
|
||||||
@ -117,7 +117,7 @@
|
|||||||
"while memEx fully supports multiple users, each memEx instance should be treated as a single cohesive and collaborative document."
|
"while memEx fully supports multiple users, each memEx instance should be treated as a single cohesive and collaborative document."
|
||||||
) %>
|
) %>
|
||||||
<%= gettext(
|
<%= gettext(
|
||||||
"note, context and pipeline slugs must be unique, and you are free to backlink to notes not written by you."
|
"note, context and pipeline slugs must be unique, and you are free to link to notes not written by you."
|
||||||
) %>
|
) %>
|
||||||
<%= gettext(
|
<%= gettext(
|
||||||
"so, i'd recommend inviting anyone you'd like to work on your collective memEx. however, when in doubt, hopefully setting up a new instance is easy enough. if it isn't, then feel free to let me know :)"
|
"so, i'd recommend inviting anyone you'd like to work on your collective memEx. however, when in doubt, hopefully setting up a new instance is easy enough. if it isn't, then feel free to let me know :)"
|
||||||
|
@ -5,13 +5,12 @@ defmodule MemexWeb.HomeLive do
|
|||||||
|
|
||||||
use MemexWeb, :live_view
|
use MemexWeb, :live_view
|
||||||
alias Memex.Accounts
|
alias Memex.Accounts
|
||||||
alias MemexWeb.FaqLive
|
|
||||||
|
|
||||||
@version Mix.Project.config()[:version]
|
@version Mix.Project.config()[:version]
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def mount(_params, _session, socket) do
|
def mount(_params, _session, socket) do
|
||||||
admins = Accounts.list_users_by_role(:admin)
|
admins = Accounts.list_users_by_role(:admin)
|
||||||
{:ok, socket |> assign(page_title: gettext("home"), admins: admins, version: @version)}
|
{:ok, socket |> assign(admins: admins, page_title: gettext("home"), version: @version)}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<div class="mx-auto flex flex-col justify-center items-stretch space-y-4 max-w-lg">
|
<div class="flex flex-col justify-center items-stretch mx-auto space-y-4 max-w-lg">
|
||||||
<h1 class="title text-primary-400 text-xl">
|
<h1 class="text-xl title text-primary-400">
|
||||||
<%= gettext("memEx") %>
|
<%= gettext("memEx") %>
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
@ -31,8 +31,8 @@
|
|||||||
</p>
|
</p>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li class="flex flex-col justify-center items-center text-right space-y-2">
|
<li class="flex flex-col justify-center items-center space-y-2 text-right">
|
||||||
<.link navigate={Routes.live_path(Endpoint, FaqLive)} class="btn btn-primary">
|
<.link navigate={~p"/faq"} class="btn btn-primary">
|
||||||
<%= gettext("read more on how to use memEx") %>
|
<%= gettext("read more on how to use memEx") %>
|
||||||
</.link>
|
</.link>
|
||||||
</li>
|
</li>
|
||||||
@ -41,7 +41,7 @@
|
|||||||
<hr class="hr" />
|
<hr class="hr" />
|
||||||
|
|
||||||
<ul class="flex flex-col space-y-4">
|
<ul class="flex flex-col space-y-4">
|
||||||
<h2 class="title text-primary-400 text-lg">
|
<h2 class="text-lg title text-primary-400">
|
||||||
<%= gettext("features") %>
|
<%= gettext("features") %>
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
@ -71,12 +71,21 @@
|
|||||||
<%= gettext("accessible from any internet-capable device") %>
|
<%= gettext("accessible from any internet-capable device") %>
|
||||||
</p>
|
</p>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
<li class="flex flex-col justify-center items-center space-y-2">
|
||||||
|
<b class="whitespace-nowrap">
|
||||||
|
<%= gettext("backlinks:") %>
|
||||||
|
</b>
|
||||||
|
<p>
|
||||||
|
<%= gettext("view referencing items from the referenced item") %>
|
||||||
|
</p>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<hr class="hr" />
|
<hr class="hr" />
|
||||||
|
|
||||||
<ul class="flex flex-col justify-center space-y-4">
|
<ul class="flex flex-col justify-center space-y-4">
|
||||||
<h2 class="title text-primary-400 text-lg">
|
<h2 class="text-lg title text-primary-400">
|
||||||
<%= gettext("instance information") %>
|
<%= gettext("instance information") %>
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
@ -86,7 +95,7 @@
|
|||||||
</b>
|
</b>
|
||||||
<p class="flex flex-col justify-center items-center space-y-2">
|
<p class="flex flex-col justify-center items-center space-y-2">
|
||||||
<%= if @admins |> Enum.empty?() do %>
|
<%= if @admins |> Enum.empty?() do %>
|
||||||
<.link href={Routes.user_registration_path(Endpoint, :new)} class="link">
|
<.link href={~p"/users/register"} class="link">
|
||||||
<%= dgettext("prompts", "register to setup memEx") %>
|
<%= dgettext("prompts", "register to setup memEx") %>
|
||||||
</.link>
|
</.link>
|
||||||
<% else %>
|
<% else %>
|
||||||
@ -124,7 +133,7 @@
|
|||||||
<hr class="hr" />
|
<hr class="hr" />
|
||||||
|
|
||||||
<ul class="flex flex-col space-y-2">
|
<ul class="flex flex-col space-y-2">
|
||||||
<h2 class="title text-primary-400 text-lg">
|
<h2 class="text-lg title text-primary-400">
|
||||||
<%= gettext("get involved") %>
|
<%= gettext("get involved") %>
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
|
@ -82,7 +82,7 @@ defmodule MemexWeb.InviteLive.FormComponent do
|
|||||||
socket |> put_flash(:info, prompt) |> push_navigate(to: return_to)
|
socket |> put_flash(:info, prompt) |> push_navigate(to: return_to)
|
||||||
|
|
||||||
{:error, %Changeset{} = changeset} ->
|
{:error, %Changeset{} = changeset} ->
|
||||||
socket |> assign(changeset: changeset)
|
socket |> assign(:changeset, changeset)
|
||||||
end
|
end
|
||||||
|
|
||||||
{:noreply, socket}
|
{:noreply, socket}
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user