mirror of
https://github.com/sysadminsmedia/homebox.git
synced 2025-12-21 21:33:02 +01:00
Compare commits
136 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
25c76522d6 | ||
|
|
c0e2aa5c62 | ||
|
|
d0b9f742ae | ||
|
|
80d56829c5 | ||
|
|
0946310f60 | ||
|
|
7c855cf55d | ||
|
|
0ab95fb670 | ||
|
|
1e81b4bab4 | ||
|
|
67c50068d9 | ||
|
|
c3628e36f7 | ||
|
|
526799c6da | ||
|
|
4ef7529533 | ||
|
|
b06d670dff | ||
|
|
02c0453ff3 | ||
|
|
09358aa5b2 | ||
|
|
dbe77ea19d | ||
|
|
85e5c7e8e7 | ||
|
|
3c273b370d | ||
|
|
343e56b440 | ||
|
|
3a949aee5a | ||
|
|
1601e52c9c | ||
|
|
760cc8e35c | ||
|
|
6051e1fb8b | ||
|
|
7b146947df | ||
|
|
5497a10f9f | ||
|
|
3e6f4b3657 | ||
|
|
7baf58ad61 | ||
|
|
d72437d18c | ||
|
|
ea57981953 | ||
|
|
c2d0cce02d | ||
|
|
9f7a119e95 | ||
|
|
0dacc97e99 | ||
|
|
52a44da56b | ||
|
|
7114f262c2 | ||
|
|
7647ea96d1 | ||
|
|
593da25cdb | ||
|
|
f22bce7ccb | ||
|
|
1688773bba | ||
|
|
b56b5d2400 | ||
|
|
33ee208071 | ||
|
|
fe880cc2c7 | ||
|
|
cffe57b74e | ||
|
|
66882d6fd9 | ||
|
|
050f22f051 | ||
|
|
7891af3a9a | ||
|
|
40cbccf50a | ||
|
|
0348da362c | ||
|
|
f0a3780f3a | ||
|
|
39163f3cfc | ||
|
|
43676ab407 | ||
|
|
639f795b9a | ||
|
|
fc95d2cab8 | ||
|
|
695b6d68e6 | ||
|
|
b6c265098d | ||
|
|
2a80d348bd | ||
|
|
c6542de93d | ||
|
|
5928678564 | ||
|
|
fac52ca122 | ||
|
|
e9d270269f | ||
|
|
0a4c5fbb28 | ||
|
|
2bfb0283d9 | ||
|
|
94e8aee36f | ||
|
|
8051956a2e | ||
|
|
c7020503be | ||
|
|
28edce96d9 | ||
|
|
b4481fcc84 | ||
|
|
2be2bebb4e | ||
|
|
d4bb8def62 | ||
|
|
7442cb01b7 | ||
|
|
95ba8275e8 | ||
|
|
2f4a0dd212 | ||
|
|
52a621e9ba | ||
|
|
1f77fad829 | ||
|
|
8d93a3f56e | ||
|
|
9c572e7ab2 | ||
|
|
1f15e74730 | ||
|
|
7570a04c02 | ||
|
|
fc2e89c448 | ||
|
|
be216ff7fe | ||
|
|
388208571b | ||
|
|
1891903007 | ||
|
|
41a7e73ff4 | ||
|
|
81d9fb0700 | ||
|
|
76312d6eb6 | ||
|
|
f31528c841 | ||
|
|
6d869fdece | ||
|
|
d0784a7773 | ||
|
|
791f843bc8 | ||
|
|
a0cdb231fd | ||
|
|
e0004842e6 | ||
|
|
fdbfa0e76f | ||
|
|
005516013f | ||
|
|
9ec3dd4b16 | ||
|
|
4dacf981a9 | ||
|
|
9f7b76b37d | ||
|
|
0d51558e74 | ||
|
|
236c257892 | ||
|
|
3540ce4297 | ||
|
|
0bcb155756 | ||
|
|
12219522ab | ||
|
|
13864997ab | ||
|
|
2ab2766534 | ||
|
|
e051352070 | ||
|
|
3bf1e50620 | ||
|
|
42f3c88396 | ||
|
|
c9f31ef934 | ||
|
|
01f54aeb52 | ||
|
|
2d1016d362 | ||
|
|
b48c961ac1 | ||
|
|
4d47567995 | ||
|
|
6b2e3accf7 | ||
|
|
c1f8520c4f | ||
|
|
41eb99ec40 | ||
|
|
97a74127fb | ||
|
|
a9396167bf | ||
|
|
3385e5684e | ||
|
|
bb9672214c | ||
|
|
1b93672417 | ||
|
|
8b1cedd4a8 | ||
|
|
2c34047b6d | ||
|
|
f0942f0714 | ||
|
|
967e574ea8 | ||
|
|
625730f37c | ||
|
|
0a72fa95b3 | ||
|
|
c39a65ec21 | ||
|
|
9403fb27e0 | ||
|
|
ab34791737 | ||
|
|
d03e60d580 | ||
|
|
6fcf9965bb | ||
|
|
d53dcd37e6 | ||
|
|
f3531cacb3 | ||
|
|
10cdca94dc | ||
|
|
0b2b7bc4fd | ||
|
|
105f63487b | ||
|
|
f3c745e42e | ||
|
|
f3e7d7a19b |
65
.github/scripts/update_currencies.py
vendored
Normal file
65
.github/scripts/update_currencies.py
vendored
Normal file
@@ -0,0 +1,65 @@
|
||||
import requests
|
||||
import json
|
||||
import os
|
||||
|
||||
def fetch_currencies():
|
||||
try:
|
||||
response = requests.get('https://restcountries.com/v3.1/all')
|
||||
response.raise_for_status()
|
||||
except requests.exceptions.Timeout:
|
||||
print("Request to the API timed out.")
|
||||
return []
|
||||
except requests.exceptions.RequestException as e:
|
||||
print(f"An error occurred while making the request: {e}")
|
||||
return []
|
||||
|
||||
try:
|
||||
countries = response.json()
|
||||
except json.JSONDecodeError:
|
||||
print("Failed to decode JSON from the response.")
|
||||
return []
|
||||
|
||||
currencies_list = []
|
||||
for country in countries:
|
||||
country_name = country.get('name', {}).get('common')
|
||||
country_currencies = country.get('currencies', {})
|
||||
for currency_code, currency_info in country_currencies.items():
|
||||
symbol = currency_info.get('symbol', '')
|
||||
currencies_list.append({
|
||||
'code': currency_code,
|
||||
'local': country_name,
|
||||
'symbol': symbol,
|
||||
'name': currency_info.get('name')
|
||||
})
|
||||
|
||||
return currencies_list
|
||||
|
||||
def save_currencies(currencies, file_path):
|
||||
try:
|
||||
os.makedirs(os.path.dirname(file_path), exist_ok=True)
|
||||
with open(file_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(currencies, f, ensure_ascii=False, indent=4)
|
||||
except IOError as e:
|
||||
print(f"An error occurred while writing to the file: {e}")
|
||||
|
||||
def load_existing_currencies(file_path):
|
||||
try:
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
except (IOError, json.JSONDecodeError):
|
||||
return [] # Return an empty list if file doesn't exist or is invalid
|
||||
|
||||
def main():
|
||||
save_path = 'backend/internal/core/currencies/currencies.json'
|
||||
|
||||
existing_currencies = load_existing_currencies(save_path)
|
||||
new_currencies = fetch_currencies()
|
||||
|
||||
if new_currencies == existing_currencies:
|
||||
print("Currencies up-to-date with API, skipping commit.")
|
||||
else:
|
||||
save_currencies(new_currencies, save_path)
|
||||
print("Currencies updated and saved.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -92,8 +92,8 @@ jobs:
|
||||
tags: ${{ steps.metadata.outputs.tags }}
|
||||
labels: ${{ steps.metadata.outputs.labels }}
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||
# cache-from: type=gha
|
||||
# cache-to: type=gha,mode=max
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
build-args: |
|
||||
VERSION=${{ github.ref_name }}
|
||||
COMMIT=${{ github.sha }}
|
||||
|
||||
4
.github/workflows/docker-publish.yaml
vendored
4
.github/workflows/docker-publish.yaml
vendored
@@ -89,8 +89,8 @@ jobs:
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||
# cache-from: type=gha
|
||||
# cache-to: type=gha,mode=max
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
build-args: |
|
||||
VERSION=${{ github.ref_name }}
|
||||
COMMIT=${{ github.sha }}
|
||||
|
||||
100
.github/workflows/update-currencies.yml
vendored
Normal file
100
.github/workflows/update-currencies.yml
vendored
Normal file
@@ -0,0 +1,100 @@
|
||||
name: Update Currencies
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
update-currencies:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: '3.8'
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install requests
|
||||
|
||||
- name: Run currency fetch script
|
||||
run: python .github/scripts/update_currencies.py
|
||||
|
||||
- name: Check for changes
|
||||
id: check_changes
|
||||
run: |
|
||||
if [[ $(git status --porcelain) ]]; then
|
||||
echo "Changes detected."
|
||||
echo "changes=true" >> $GITHUB_ENV
|
||||
else
|
||||
echo "No changes detected."
|
||||
echo "changes=false" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
- name: Delete existing update-currencies branch
|
||||
run: |
|
||||
if git show-ref --verify --quiet refs/heads/update-currencies; then
|
||||
git branch -D update-currencies
|
||||
echo "Deleted existing update-currencies branch."
|
||||
else
|
||||
echo "No existing update-currencies branch to delete."
|
||||
fi
|
||||
|
||||
- name: Create new update-currencies branch
|
||||
if: env.changes == 'true'
|
||||
run: |
|
||||
git config --global user.name "github-actions[bot]"
|
||||
git config --global user.email "github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
# Create a new branch
|
||||
git checkout -b update-currencies
|
||||
git add backend/internal/core/currencies/currencies.json
|
||||
git commit -m "Update currencies.json"
|
||||
|
||||
# Fetch the latest changes from the remote
|
||||
git fetch origin
|
||||
|
||||
# Attempt to rebase with the latest changes
|
||||
if git show-ref --verify --quiet refs/remotes/origin/update-currencies; then
|
||||
if ! git rebase origin/update-currencies; then
|
||||
echo "Rebase conflicts occurred. Please resolve them manually."
|
||||
echo "To resolve conflicts, check out the 'update-currencies' branch locally."
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "No existing remote branch 'update-currencies'. Skipping rebase."
|
||||
fi
|
||||
|
||||
# Push the new branch to the remote
|
||||
if ! git push --set-upstream origin update-currencies; then
|
||||
echo "Push failed, trying to fetch and rebase again."
|
||||
git fetch origin
|
||||
if git show-ref --verify --quiet refs/remotes/origin/update-currencies; then
|
||||
if ! git rebase origin/update-currencies; then
|
||||
echo "Second rebase failed. Please resolve manually."
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "No existing remote branch 'update-currencies'. Skipping rebase."
|
||||
fi
|
||||
if ! git push --set-upstream origin update-currencies; then
|
||||
echo "Second push failed. Please resolve manually."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Create a pull request
|
||||
curl -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
|
||||
-X POST \
|
||||
-d '{"title": "Update currencies", "head": "update-currencies", "base": "main"}' \
|
||||
https://api.github.com/repos/${{ github.repository }}/pulls
|
||||
|
||||
- name: Notify no changes
|
||||
if: env.changes == 'false'
|
||||
run: echo "Currencies up-to-date with API, skipping commit."
|
||||
21
Dockerfile
21
Dockerfile
@@ -1,13 +1,23 @@
|
||||
# Node dependencies
|
||||
FROM node:18-alpine AS frontend-dependencies
|
||||
WORKDIR /app
|
||||
RUN npm install -g pnpm
|
||||
COPY frontend/package.json frontend/pnpm-lock.yaml ./
|
||||
RUN pnpm install --frozen-lockfile --shamefully-hoist
|
||||
|
||||
# Build Nuxt
|
||||
FROM node:18-alpine AS frontend-builder
|
||||
WORKDIR /app
|
||||
RUN npm install -g pnpm
|
||||
COPY frontend/package.json frontend/pnpm-lock.yaml ./
|
||||
RUN pnpm install --frozen-lockfile --shamefully-hoist
|
||||
COPY frontend .
|
||||
COPY --from=frontend-dependencies /app/node_modules ./node_modules
|
||||
RUN pnpm build
|
||||
|
||||
FROM golang:alpine AS builder-dependencies
|
||||
WORKDIR /go/src/app
|
||||
COPY ./backend .
|
||||
RUN go mod download
|
||||
|
||||
# Build API
|
||||
FROM golang:alpine AS builder
|
||||
ARG BUILD_TIME
|
||||
@@ -19,10 +29,11 @@ RUN apk update && \
|
||||
|
||||
WORKDIR /go/src/app
|
||||
COPY ./backend .
|
||||
RUN go get -d -v ./...
|
||||
RUN rm -rf ./app/api/public
|
||||
COPY --from=frontend-builder /app/.output/public ./app/api/static/public
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build \
|
||||
COPY --from=builder-dependencies /go/pkg/mod /go/pkg/mod
|
||||
RUN --mount=type=cache,target=/root/.cache/go-build \
|
||||
CGO_ENABLED=0 GOOS=linux go build \
|
||||
-ldflags "-s -w -X main.commit=$COMMIT -X main.buildTime=$BUILD_TIME -X main.version=$VERSION" \
|
||||
-o /go/bin/api \
|
||||
-v ./app/api/*.go
|
||||
@@ -51,7 +62,7 @@ HEALTHCHECK --interval=30s \
|
||||
--timeout=5s \
|
||||
--start-period=5s \
|
||||
--retries=3 \
|
||||
CMD [ "/usr/bin/wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:7745/api/v1/status" ]
|
||||
CMD [ "/usr/bin/wget", "--no-verbose", "--tries=1", "-O -", "http://localhost:7745/api/v1/status" ]
|
||||
VOLUME [ "/data" ]
|
||||
|
||||
ENTRYPOINT [ "/app/api" ]
|
||||
|
||||
@@ -1,35 +1,42 @@
|
||||
# Node dependencies
|
||||
FROM node:18-alpine AS frontend-dependencies
|
||||
WORKDIR /app
|
||||
RUN npm install -g pnpm
|
||||
COPY frontend/package.json frontend/pnpm-lock.yaml ./
|
||||
RUN pnpm install --frozen-lockfile --shamefully-hoist
|
||||
|
||||
# Build Nuxt
|
||||
FROM node:18-alpine AS frontend-builder
|
||||
WORKDIR /app
|
||||
RUN npm install -g pnpm
|
||||
COPY frontend/package.json frontend/pnpm-lock.yaml ./
|
||||
RUN pnpm install --frozen-lockfile --shamefully-hoist
|
||||
COPY frontend .
|
||||
COPY --from=frontend-dependencies /app/node_modules ./node_modules
|
||||
RUN pnpm build
|
||||
|
||||
FROM golang:alpine AS builder-dependencies
|
||||
WORKDIR /go/src/app
|
||||
COPY ./backend .
|
||||
RUN go mod download
|
||||
|
||||
# Build API
|
||||
FROM golang:alpine AS builder
|
||||
ARG BUILD_TIME
|
||||
ARG COMMIT
|
||||
ARG VERSION
|
||||
ARG BUSYBOX_VERSION=1.36.1-r31
|
||||
RUN apk update && \
|
||||
apk upgrade && \
|
||||
apk add --update git build-base gcc g++
|
||||
|
||||
WORKDIR /go/src/app
|
||||
COPY ./backend .
|
||||
RUN go get -d -v ./...
|
||||
RUN rm -rf ./app/api/public
|
||||
COPY --from=frontend-builder /app/.output/public ./app/api/static/public
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build \
|
||||
COPY --from=builder-dependencies /go/pkg/mod /go/pkg/mod
|
||||
RUN --mount=type=cache,target=/root/.cache/go-build \
|
||||
CGO_ENABLED=0 GOOS=linux go build \
|
||||
-ldflags "-s -w -X main.commit=$COMMIT -X main.buildTime=$BUILD_TIME -X main.version=$VERSION" \
|
||||
-o /go/bin/api \
|
||||
-v ./app/api/*.go && \
|
||||
chmod +x /go/bin/api && \
|
||||
# create a directory so that we can copy it in the next stage
|
||||
mkdir /data
|
||||
-v ./app/api/*.go
|
||||
|
||||
FROM gcr.io/distroless/java:latest
|
||||
|
||||
@@ -54,7 +61,7 @@ HEALTHCHECK --interval=30s \
|
||||
--timeout=5s \
|
||||
--start-period=5s \
|
||||
--retries=3 \
|
||||
CMD [ "/usr/bin/wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:7745/api/v1/status" ]
|
||||
CMD [ "/usr/bin/wget", "--no-verbose", "--tries=1", "-O -", "http://localhost:7745/api/v1/status" ]
|
||||
VOLUME [ "/data" ]
|
||||
|
||||
# Drop root and run as low-privileged user
|
||||
|
||||
@@ -16,9 +16,11 @@
|
||||
Homebox is the inventory and organization system built for the Home User! With a focus on simplicity and ease of use, Homebox is the perfect solution for your home inventory, organization, and management needs. While developing this project, I've tried to keep the following principles in mind:
|
||||
|
||||
- _Simple_ - Homebox is designed to be simple and easy to use. No complicated setup or configuration required. Use either a single docker container, or deploy yourself by compiling the binary for your platform of choice.
|
||||
- _Blazingly Fast_ - Homebox is written in Go, which makes it extremely fast and requires minimal resources to deploy. In general idle memory usage is less than 50MB for the whole container.
|
||||
- _Blazingly Fast_ - Homebox is written in Go, which makes it extremely fast and requires minimal resources to deploy. In general, idle memory usage is less than 50MB for the whole container.
|
||||
- _Portable_ - Homebox is designed to be portable and run on anywhere. We use SQLite and an embedded Web UI to make it easy to deploy, use, and backup.
|
||||
|
||||
# Screenshots
|
||||
Check out screenshots of the project [here](https://imgur.com/a/5gLWt2j).
|
||||
|
||||
## Quick Start
|
||||
|
||||
@@ -47,6 +49,11 @@ Contributions are what make the open source community such an amazing place to l
|
||||
|
||||
If you are not a coder, you can still contribute financially. Financial contributions help me prioritize working on this project over others and helps me know that there is a real demand for project development.
|
||||
|
||||
## Help us Translate
|
||||
We want to make sure that Homebox is available in as many languages as possible. If you are interested in helping us translate Homebox, please help us via our [Weblate instance](https://translate.sysadminsmedia.com/projects/homebox/).
|
||||
|
||||
[](http://translate.sysadminsmedia.com/engage/homebox/)
|
||||
|
||||
## Credits
|
||||
|
||||
- Original project by [@hay-kot](https://github.com/hay-kot)
|
||||
|
||||
@@ -57,6 +57,12 @@ func WithSecureCookies(secure bool) func(*V1Controller) {
|
||||
}
|
||||
}
|
||||
|
||||
func WithURL(url string) func(*V1Controller) {
|
||||
return func(ctrl *V1Controller) {
|
||||
ctrl.url = url
|
||||
}
|
||||
}
|
||||
|
||||
type V1Controller struct {
|
||||
cookieSecure bool
|
||||
repo *repo.AllRepos
|
||||
@@ -65,6 +71,7 @@ type V1Controller struct {
|
||||
isDemo bool
|
||||
allowRegistration bool
|
||||
bus *eventbus.EventBus
|
||||
url string
|
||||
}
|
||||
|
||||
type (
|
||||
@@ -87,12 +94,6 @@ type (
|
||||
}
|
||||
)
|
||||
|
||||
func BaseURLFunc(prefix string) func(s string) string {
|
||||
return func(s string) string {
|
||||
return prefix + "/v1" + s
|
||||
}
|
||||
}
|
||||
|
||||
func NewControllerV1(svc *services.AllServices, repos *repo.AllRepos, bus *eventbus.EventBus, options ...func(*V1Controller)) *V1Controller {
|
||||
ctrl := &V1Controller{
|
||||
repo: repos,
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"errors"
|
||||
"math/big"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/google/uuid"
|
||||
@@ -333,7 +334,7 @@ func (ctrl *V1Controller) HandleItemsExport() errchain.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) error {
|
||||
ctx := services.NewContext(r.Context())
|
||||
|
||||
csvData, err := ctrl.svc.Items.ExportCSV(r.Context(), ctx.GID)
|
||||
csvData, err := ctrl.svc.Items.ExportCSV(r.Context(), ctx.GID, getHBURL(r.Header.Get("Referer"), ctrl.url))
|
||||
if err != nil {
|
||||
log.Err(err).Msg("failed to export items")
|
||||
return validate.NewRequestError(err, http.StatusInternalServerError)
|
||||
@@ -347,3 +348,26 @@ func (ctrl *V1Controller) HandleItemsExport() errchain.HandlerFunc {
|
||||
return writer.WriteAll(csvData)
|
||||
}
|
||||
}
|
||||
|
||||
func getHBURL(refererHeader, fallback string) (hbURL string) {
|
||||
hbURL = refererHeader
|
||||
if hbURL == "" {
|
||||
hbURL = fallback
|
||||
}
|
||||
|
||||
return stripPathFromURL(hbURL)
|
||||
}
|
||||
|
||||
// stripPathFromURL removes the path from a URL.
|
||||
// ex. https://example.com/tools -> https://example.com
|
||||
func stripPathFromURL(rawURL string) string {
|
||||
parsedURL, err := url.Parse(rawURL)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("failed to parse URL")
|
||||
return ""
|
||||
}
|
||||
|
||||
strippedURL := url.URL{Scheme: parsedURL.Scheme, Host: parsedURL.Host}
|
||||
|
||||
return strippedURL.String()
|
||||
}
|
||||
|
||||
@@ -87,12 +87,22 @@ func (ctrl *V1Controller) HandleLocationDelete() errchain.HandlerFunc {
|
||||
|
||||
func (ctrl *V1Controller) GetLocationWithPrice(auth context.Context, GID uuid.UUID, ID uuid.UUID) (repo.LocationOut, error) {
|
||||
var location, err = ctrl.repo.Locations.GetOneByGroup(auth, GID, ID)
|
||||
if err != nil {
|
||||
return repo.LocationOut{}, err
|
||||
}
|
||||
|
||||
// Add direct child items price
|
||||
totalPrice := new(big.Int)
|
||||
items, err := ctrl.repo.Items.QueryByGroup(auth, GID, repo.ItemQuery{LocationIDs: []uuid.UUID{ID}})
|
||||
if err != nil {
|
||||
return repo.LocationOut{}, err
|
||||
}
|
||||
|
||||
for _, item := range items.Items {
|
||||
totalPrice.Add(totalPrice, big.NewInt(int64(item.PurchasePrice*100)))
|
||||
// Convert item.Quantity to float64 for multiplication
|
||||
quantity := float64(item.Quantity)
|
||||
itemTotal := big.NewInt(int64(item.PurchasePrice * quantity * 100))
|
||||
totalPrice.Add(totalPrice, itemTotal)
|
||||
}
|
||||
|
||||
totalPriceFloat := new(big.Float).SetInt(totalPrice)
|
||||
@@ -101,14 +111,15 @@ func (ctrl *V1Controller) GetLocationWithPrice(auth context.Context, GID uuid.UU
|
||||
|
||||
// Add price from child locations
|
||||
for _, childLocation := range location.Children {
|
||||
var childLocation, err = ctrl.GetLocationWithPrice(auth, GID, childLocation.ID)
|
||||
var childLocationWithPrice repo.LocationOut
|
||||
childLocationWithPrice, err = ctrl.GetLocationWithPrice(auth, GID, childLocation.ID)
|
||||
if err != nil {
|
||||
return repo.LocationOut{}, err
|
||||
}
|
||||
location.TotalPrice += childLocation.TotalPrice
|
||||
location.TotalPrice += childLocationWithPrice.TotalPrice
|
||||
}
|
||||
|
||||
return location, err
|
||||
return location, nil
|
||||
}
|
||||
|
||||
// HandleLocationGet godoc
|
||||
|
||||
@@ -3,12 +3,7 @@ package main
|
||||
import (
|
||||
"embed"
|
||||
"errors"
|
||||
"io"
|
||||
"mime"
|
||||
"net/http"
|
||||
"path"
|
||||
"path/filepath"
|
||||
|
||||
"fmt"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/hay-kot/httpkit/errchain"
|
||||
httpSwagger "github.com/swaggo/http-swagger/v2" // http-swagger middleware
|
||||
@@ -18,6 +13,11 @@ import (
|
||||
_ "github.com/sysadminsmedia/homebox/backend/app/api/static/docs"
|
||||
"github.com/sysadminsmedia/homebox/backend/internal/data/ent/authroles"
|
||||
"github.com/sysadminsmedia/homebox/backend/internal/data/repo"
|
||||
"io"
|
||||
"mime"
|
||||
"net/http"
|
||||
"path"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
const prefix = "/api"
|
||||
@@ -47,8 +47,6 @@ func (a *app) mountRoutes(r *chi.Mux, chain *errchain.ErrChain, repos *repo.AllR
|
||||
// =========================================================================
|
||||
// API Version 1
|
||||
|
||||
v1Base := v1.BaseURLFunc(prefix)
|
||||
|
||||
v1Ctrl := v1.NewControllerV1(
|
||||
a.services,
|
||||
a.repos,
|
||||
@@ -56,94 +54,96 @@ func (a *app) mountRoutes(r *chi.Mux, chain *errchain.ErrChain, repos *repo.AllR
|
||||
v1.WithMaxUploadSize(a.conf.Web.MaxUploadSize),
|
||||
v1.WithRegistration(a.conf.Options.AllowRegistration),
|
||||
v1.WithDemoStatus(a.conf.Demo), // Disable Password Change in Demo Mode
|
||||
v1.WithURL(fmt.Sprintf("%s:%s", a.conf.Web.Host, a.conf.Web.Port)),
|
||||
)
|
||||
|
||||
r.Get(v1Base("/status"), chain.ToHandlerFunc(v1Ctrl.HandleBase(func() bool { return true }, v1.Build{
|
||||
r.Route(prefix+"/v1", func(r chi.Router) {
|
||||
r.Get("/status", chain.ToHandlerFunc(v1Ctrl.HandleBase(func() bool { return true }, v1.Build{
|
||||
Version: version,
|
||||
Commit: commit,
|
||||
BuildTime: buildTime,
|
||||
})))
|
||||
|
||||
r.Get(v1Base("/currencies"), chain.ToHandlerFunc(v1Ctrl.HandleCurrency()))
|
||||
r.Get("/currencies", chain.ToHandlerFunc(v1Ctrl.HandleCurrency()))
|
||||
|
||||
providers := []v1.AuthProvider{
|
||||
providers.NewLocalProvider(a.services.User),
|
||||
}
|
||||
|
||||
r.Post(v1Base("/users/register"), chain.ToHandlerFunc(v1Ctrl.HandleUserRegistration()))
|
||||
r.Post(v1Base("/users/login"), chain.ToHandlerFunc(v1Ctrl.HandleAuthLogin(providers...)))
|
||||
r.Post("/users/register", chain.ToHandlerFunc(v1Ctrl.HandleUserRegistration()))
|
||||
r.Post("/users/login", chain.ToHandlerFunc(v1Ctrl.HandleAuthLogin(providers...)))
|
||||
|
||||
userMW := []errchain.Middleware{
|
||||
a.mwAuthToken,
|
||||
a.mwRoles(RoleModeOr, authroles.RoleUser.String()),
|
||||
}
|
||||
|
||||
r.Get(v1Base("/ws/events"), chain.ToHandlerFunc(v1Ctrl.HandleCacheWS(), userMW...))
|
||||
r.Get(v1Base("/users/self"), chain.ToHandlerFunc(v1Ctrl.HandleUserSelf(), userMW...))
|
||||
r.Put(v1Base("/users/self"), chain.ToHandlerFunc(v1Ctrl.HandleUserSelfUpdate(), userMW...))
|
||||
r.Delete(v1Base("/users/self"), chain.ToHandlerFunc(v1Ctrl.HandleUserSelfDelete(), userMW...))
|
||||
r.Post(v1Base("/users/logout"), chain.ToHandlerFunc(v1Ctrl.HandleAuthLogout(), userMW...))
|
||||
r.Get(v1Base("/users/refresh"), chain.ToHandlerFunc(v1Ctrl.HandleAuthRefresh(), userMW...))
|
||||
r.Put(v1Base("/users/self/change-password"), chain.ToHandlerFunc(v1Ctrl.HandleUserSelfChangePassword(), userMW...))
|
||||
r.Get("/ws/events", chain.ToHandlerFunc(v1Ctrl.HandleCacheWS(), userMW...))
|
||||
r.Get("/users/self", chain.ToHandlerFunc(v1Ctrl.HandleUserSelf(), userMW...))
|
||||
r.Put("/users/self", chain.ToHandlerFunc(v1Ctrl.HandleUserSelfUpdate(), userMW...))
|
||||
r.Delete("/users/self", chain.ToHandlerFunc(v1Ctrl.HandleUserSelfDelete(), userMW...))
|
||||
r.Post("/users/logout", chain.ToHandlerFunc(v1Ctrl.HandleAuthLogout(), userMW...))
|
||||
r.Get("/users/refresh", chain.ToHandlerFunc(v1Ctrl.HandleAuthRefresh(), userMW...))
|
||||
r.Put("/users/self/change-password", chain.ToHandlerFunc(v1Ctrl.HandleUserSelfChangePassword(), userMW...))
|
||||
|
||||
r.Post(v1Base("/groups/invitations"), chain.ToHandlerFunc(v1Ctrl.HandleGroupInvitationsCreate(), userMW...))
|
||||
r.Get(v1Base("/groups/statistics"), chain.ToHandlerFunc(v1Ctrl.HandleGroupStatistics(), userMW...))
|
||||
r.Get(v1Base("/groups/statistics/purchase-price"), chain.ToHandlerFunc(v1Ctrl.HandleGroupStatisticsPriceOverTime(), userMW...))
|
||||
r.Get(v1Base("/groups/statistics/locations"), chain.ToHandlerFunc(v1Ctrl.HandleGroupStatisticsLocations(), userMW...))
|
||||
r.Get(v1Base("/groups/statistics/labels"), chain.ToHandlerFunc(v1Ctrl.HandleGroupStatisticsLabels(), userMW...))
|
||||
r.Post("/groups/invitations", chain.ToHandlerFunc(v1Ctrl.HandleGroupInvitationsCreate(), userMW...))
|
||||
r.Get("/groups/statistics", chain.ToHandlerFunc(v1Ctrl.HandleGroupStatistics(), userMW...))
|
||||
r.Get("/groups/statistics/purchase-price", chain.ToHandlerFunc(v1Ctrl.HandleGroupStatisticsPriceOverTime(), userMW...))
|
||||
r.Get("/groups/statistics/locations", chain.ToHandlerFunc(v1Ctrl.HandleGroupStatisticsLocations(), userMW...))
|
||||
r.Get("/groups/statistics/labels", chain.ToHandlerFunc(v1Ctrl.HandleGroupStatisticsLabels(), userMW...))
|
||||
|
||||
// TODO: I don't like /groups being the URL for users
|
||||
r.Get(v1Base("/groups"), chain.ToHandlerFunc(v1Ctrl.HandleGroupGet(), userMW...))
|
||||
r.Put(v1Base("/groups"), chain.ToHandlerFunc(v1Ctrl.HandleGroupUpdate(), userMW...))
|
||||
r.Get("/groups", chain.ToHandlerFunc(v1Ctrl.HandleGroupGet(), userMW...))
|
||||
r.Put("/groups", chain.ToHandlerFunc(v1Ctrl.HandleGroupUpdate(), userMW...))
|
||||
|
||||
r.Post(v1Base("/actions/ensure-asset-ids"), chain.ToHandlerFunc(v1Ctrl.HandleEnsureAssetID(), userMW...))
|
||||
r.Post(v1Base("/actions/zero-item-time-fields"), chain.ToHandlerFunc(v1Ctrl.HandleItemDateZeroOut(), userMW...))
|
||||
r.Post(v1Base("/actions/ensure-import-refs"), chain.ToHandlerFunc(v1Ctrl.HandleEnsureImportRefs(), userMW...))
|
||||
r.Post(v1Base("/actions/set-primary-photos"), chain.ToHandlerFunc(v1Ctrl.HandleSetPrimaryPhotos(), userMW...))
|
||||
r.Post("/actions/ensure-asset-ids", chain.ToHandlerFunc(v1Ctrl.HandleEnsureAssetID(), userMW...))
|
||||
r.Post("/actions/zero-item-time-fields", chain.ToHandlerFunc(v1Ctrl.HandleItemDateZeroOut(), userMW...))
|
||||
r.Post("/actions/ensure-import-refs", chain.ToHandlerFunc(v1Ctrl.HandleEnsureImportRefs(), userMW...))
|
||||
r.Post("/actions/set-primary-photos", chain.ToHandlerFunc(v1Ctrl.HandleSetPrimaryPhotos(), userMW...))
|
||||
|
||||
r.Get(v1Base("/locations"), chain.ToHandlerFunc(v1Ctrl.HandleLocationGetAll(), userMW...))
|
||||
r.Post(v1Base("/locations"), chain.ToHandlerFunc(v1Ctrl.HandleLocationCreate(), userMW...))
|
||||
r.Get(v1Base("/locations/tree"), chain.ToHandlerFunc(v1Ctrl.HandleLocationTreeQuery(), userMW...))
|
||||
r.Get(v1Base("/locations/{id}"), chain.ToHandlerFunc(v1Ctrl.HandleLocationGet(), userMW...))
|
||||
r.Put(v1Base("/locations/{id}"), chain.ToHandlerFunc(v1Ctrl.HandleLocationUpdate(), userMW...))
|
||||
r.Delete(v1Base("/locations/{id}"), chain.ToHandlerFunc(v1Ctrl.HandleLocationDelete(), userMW...))
|
||||
r.Get("/locations", chain.ToHandlerFunc(v1Ctrl.HandleLocationGetAll(), userMW...))
|
||||
r.Post("/locations", chain.ToHandlerFunc(v1Ctrl.HandleLocationCreate(), userMW...))
|
||||
r.Get("/locations/tree", chain.ToHandlerFunc(v1Ctrl.HandleLocationTreeQuery(), userMW...))
|
||||
r.Get("/locations/{id}", chain.ToHandlerFunc(v1Ctrl.HandleLocationGet(), userMW...))
|
||||
r.Put("/locations/{id}", chain.ToHandlerFunc(v1Ctrl.HandleLocationUpdate(), userMW...))
|
||||
r.Delete("/locations/{id}", chain.ToHandlerFunc(v1Ctrl.HandleLocationDelete(), userMW...))
|
||||
|
||||
r.Get(v1Base("/labels"), chain.ToHandlerFunc(v1Ctrl.HandleLabelsGetAll(), userMW...))
|
||||
r.Post(v1Base("/labels"), chain.ToHandlerFunc(v1Ctrl.HandleLabelsCreate(), userMW...))
|
||||
r.Get(v1Base("/labels/{id}"), chain.ToHandlerFunc(v1Ctrl.HandleLabelGet(), userMW...))
|
||||
r.Put(v1Base("/labels/{id}"), chain.ToHandlerFunc(v1Ctrl.HandleLabelUpdate(), userMW...))
|
||||
r.Delete(v1Base("/labels/{id}"), chain.ToHandlerFunc(v1Ctrl.HandleLabelDelete(), userMW...))
|
||||
r.Get("/labels", chain.ToHandlerFunc(v1Ctrl.HandleLabelsGetAll(), userMW...))
|
||||
r.Post("/labels", chain.ToHandlerFunc(v1Ctrl.HandleLabelsCreate(), userMW...))
|
||||
r.Get("/labels/{id}", chain.ToHandlerFunc(v1Ctrl.HandleLabelGet(), userMW...))
|
||||
r.Put("/labels/{id}", chain.ToHandlerFunc(v1Ctrl.HandleLabelUpdate(), userMW...))
|
||||
r.Delete("/labels/{id}", chain.ToHandlerFunc(v1Ctrl.HandleLabelDelete(), userMW...))
|
||||
|
||||
r.Get(v1Base("/items"), chain.ToHandlerFunc(v1Ctrl.HandleItemsGetAll(), userMW...))
|
||||
r.Post(v1Base("/items"), chain.ToHandlerFunc(v1Ctrl.HandleItemsCreate(), userMW...))
|
||||
r.Post(v1Base("/items/import"), chain.ToHandlerFunc(v1Ctrl.HandleItemsImport(), userMW...))
|
||||
r.Get(v1Base("/items/export"), chain.ToHandlerFunc(v1Ctrl.HandleItemsExport(), userMW...))
|
||||
r.Get(v1Base("/items/fields"), chain.ToHandlerFunc(v1Ctrl.HandleGetAllCustomFieldNames(), userMW...))
|
||||
r.Get(v1Base("/items/fields/values"), chain.ToHandlerFunc(v1Ctrl.HandleGetAllCustomFieldValues(), userMW...))
|
||||
r.Get("/items", chain.ToHandlerFunc(v1Ctrl.HandleItemsGetAll(), userMW...))
|
||||
r.Post("/items", chain.ToHandlerFunc(v1Ctrl.HandleItemsCreate(), userMW...))
|
||||
r.Post("/items/import", chain.ToHandlerFunc(v1Ctrl.HandleItemsImport(), userMW...))
|
||||
r.Get("/items/export", chain.ToHandlerFunc(v1Ctrl.HandleItemsExport(), userMW...))
|
||||
r.Get("/items/fields", chain.ToHandlerFunc(v1Ctrl.HandleGetAllCustomFieldNames(), userMW...))
|
||||
r.Get("/items/fields/values", chain.ToHandlerFunc(v1Ctrl.HandleGetAllCustomFieldValues(), userMW...))
|
||||
|
||||
r.Get(v1Base("/items/{id}"), chain.ToHandlerFunc(v1Ctrl.HandleItemGet(), userMW...))
|
||||
r.Get(v1Base("/items/{id}/path"), chain.ToHandlerFunc(v1Ctrl.HandleItemFullPath(), userMW...))
|
||||
r.Put(v1Base("/items/{id}"), chain.ToHandlerFunc(v1Ctrl.HandleItemUpdate(), userMW...))
|
||||
r.Patch(v1Base("/items/{id}"), chain.ToHandlerFunc(v1Ctrl.HandleItemPatch(), userMW...))
|
||||
r.Delete(v1Base("/items/{id}"), chain.ToHandlerFunc(v1Ctrl.HandleItemDelete(), userMW...))
|
||||
r.Get("/items/{id}", chain.ToHandlerFunc(v1Ctrl.HandleItemGet(), userMW...))
|
||||
r.Get("/items/{id}/path", chain.ToHandlerFunc(v1Ctrl.HandleItemFullPath(), userMW...))
|
||||
r.Put("/items/{id}", chain.ToHandlerFunc(v1Ctrl.HandleItemUpdate(), userMW...))
|
||||
r.Patch("/items/{id}", chain.ToHandlerFunc(v1Ctrl.HandleItemPatch(), userMW...))
|
||||
r.Delete("/items/{id}", chain.ToHandlerFunc(v1Ctrl.HandleItemDelete(), userMW...))
|
||||
|
||||
r.Post(v1Base("/items/{id}/attachments"), chain.ToHandlerFunc(v1Ctrl.HandleItemAttachmentCreate(), userMW...))
|
||||
r.Put(v1Base("/items/{id}/attachments/{attachment_id}"), chain.ToHandlerFunc(v1Ctrl.HandleItemAttachmentUpdate(), userMW...))
|
||||
r.Delete(v1Base("/items/{id}/attachments/{attachment_id}"), chain.ToHandlerFunc(v1Ctrl.HandleItemAttachmentDelete(), userMW...))
|
||||
r.Post("/items/{id}/attachments", chain.ToHandlerFunc(v1Ctrl.HandleItemAttachmentCreate(), userMW...))
|
||||
r.Put("/items/{id}/attachments/{attachment_id}", chain.ToHandlerFunc(v1Ctrl.HandleItemAttachmentUpdate(), userMW...))
|
||||
r.Delete("/items/{id}/attachments/{attachment_id}", chain.ToHandlerFunc(v1Ctrl.HandleItemAttachmentDelete(), userMW...))
|
||||
|
||||
r.Get(v1Base("/items/{id}/maintenance"), chain.ToHandlerFunc(v1Ctrl.HandleMaintenanceLogGet(), userMW...))
|
||||
r.Post(v1Base("/items/{id}/maintenance"), chain.ToHandlerFunc(v1Ctrl.HandleMaintenanceEntryCreate(), userMW...))
|
||||
r.Put(v1Base("/items/{id}/maintenance/{entry_id}"), chain.ToHandlerFunc(v1Ctrl.HandleMaintenanceEntryUpdate(), userMW...))
|
||||
r.Delete(v1Base("/items/{id}/maintenance/{entry_id}"), chain.ToHandlerFunc(v1Ctrl.HandleMaintenanceEntryDelete(), userMW...))
|
||||
r.Get("/items/{id}/maintenance", chain.ToHandlerFunc(v1Ctrl.HandleMaintenanceLogGet(), userMW...))
|
||||
r.Post("/items/{id}/maintenance", chain.ToHandlerFunc(v1Ctrl.HandleMaintenanceEntryCreate(), userMW...))
|
||||
r.Put("/items/{id}/maintenance/{entry_id}", chain.ToHandlerFunc(v1Ctrl.HandleMaintenanceEntryUpdate(), userMW...))
|
||||
r.Delete("/items/{id}/maintenance/{entry_id}", chain.ToHandlerFunc(v1Ctrl.HandleMaintenanceEntryDelete(), userMW...))
|
||||
|
||||
r.Get(v1Base("/assets/{id}"), chain.ToHandlerFunc(v1Ctrl.HandleAssetGet(), userMW...))
|
||||
r.Get("/assets/{id}", chain.ToHandlerFunc(v1Ctrl.HandleAssetGet(), userMW...))
|
||||
|
||||
// Notifiers
|
||||
r.Get(v1Base("/notifiers"), chain.ToHandlerFunc(v1Ctrl.HandleGetUserNotifiers(), userMW...))
|
||||
r.Post(v1Base("/notifiers"), chain.ToHandlerFunc(v1Ctrl.HandleCreateNotifier(), userMW...))
|
||||
r.Put(v1Base("/notifiers/{id}"), chain.ToHandlerFunc(v1Ctrl.HandleUpdateNotifier(), userMW...))
|
||||
r.Delete(v1Base("/notifiers/{id}"), chain.ToHandlerFunc(v1Ctrl.HandleDeleteNotifier(), userMW...))
|
||||
r.Post(v1Base("/notifiers/test"), chain.ToHandlerFunc(v1Ctrl.HandlerNotifierTest(), userMW...))
|
||||
r.Get("/notifiers", chain.ToHandlerFunc(v1Ctrl.HandleGetUserNotifiers(), userMW...))
|
||||
r.Post("/notifiers", chain.ToHandlerFunc(v1Ctrl.HandleCreateNotifier(), userMW...))
|
||||
r.Put("/notifiers/{id}", chain.ToHandlerFunc(v1Ctrl.HandleUpdateNotifier(), userMW...))
|
||||
r.Delete("/notifiers/{id}", chain.ToHandlerFunc(v1Ctrl.HandleDeleteNotifier(), userMW...))
|
||||
r.Post("/notifiers/test", chain.ToHandlerFunc(v1Ctrl.HandlerNotifierTest(), userMW...))
|
||||
|
||||
// Asset-Like endpoints
|
||||
assetMW := []errchain.Middleware{
|
||||
@@ -151,17 +151,17 @@ func (a *app) mountRoutes(r *chi.Mux, chain *errchain.ErrChain, repos *repo.AllR
|
||||
a.mwRoles(RoleModeOr, authroles.RoleUser.String(), authroles.RoleAttachments.String()),
|
||||
}
|
||||
|
||||
r.Get("/qrcode", chain.ToHandlerFunc(v1Ctrl.HandleGenerateQRCode(), assetMW...))
|
||||
r.Get(
|
||||
v1Base("/qrcode"),
|
||||
chain.ToHandlerFunc(v1Ctrl.HandleGenerateQRCode(), assetMW...),
|
||||
)
|
||||
r.Get(
|
||||
v1Base("/items/{id}/attachments/{attachment_id}"),
|
||||
"/items/{id}/attachments/{attachment_id}",
|
||||
chain.ToHandlerFunc(v1Ctrl.HandleItemAttachmentGet(), assetMW...),
|
||||
)
|
||||
|
||||
// Reporting Services
|
||||
r.Get(v1Base("/reporting/bill-of-materials"), chain.ToHandlerFunc(v1Ctrl.HandleBillOfMaterialsExport(), userMW...))
|
||||
r.Get("/reporting/bill-of-materials", chain.ToHandlerFunc(v1Ctrl.HandleBillOfMaterialsExport(), userMW...))
|
||||
|
||||
r.NotFound(http.NotFound)
|
||||
})
|
||||
|
||||
r.NotFound(chain.ToHandlerFunc(notFoundHandler()))
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -18,6 +18,7 @@ type ExportCSVRow struct {
|
||||
LabelStr LabelString `csv:"HB.labels"`
|
||||
AssetID repo.AssetID `csv:"HB.asset_id"`
|
||||
Archived bool `csv:"HB.archived"`
|
||||
URL string `csv:"HB.url"`
|
||||
|
||||
Name string `csv:"HB.name"`
|
||||
Quantity int `csv:"HB.quantity"`
|
||||
|
||||
@@ -153,7 +153,7 @@ func (s *IOSheet) Read(data io.Reader) error {
|
||||
}
|
||||
|
||||
// ReadItems writes the sheet to a writer.
|
||||
func (s *IOSheet) ReadItems(ctx context.Context, items []repo.ItemOut, GID uuid.UUID, repos *repo.AllRepos) error {
|
||||
func (s *IOSheet) ReadItems(ctx context.Context, items []repo.ItemOut, GID uuid.UUID, repos *repo.AllRepos, hbURL string) error {
|
||||
s.Rows = make([]ExportCSVRow, len(items))
|
||||
|
||||
extraHeaders := map[string]struct{}{}
|
||||
@@ -178,6 +178,8 @@ func (s *IOSheet) ReadItems(ctx context.Context, items []repo.ItemOut, GID uuid.
|
||||
labelString[i] = l.Name
|
||||
}
|
||||
|
||||
url := generateItemURL(item, hbURL)
|
||||
|
||||
customFields := make([]ExportItemFields, len(item.Fields))
|
||||
|
||||
for i, f := range item.Fields {
|
||||
@@ -201,6 +203,7 @@ func (s *IOSheet) ReadItems(ctx context.Context, items []repo.ItemOut, GID uuid.
|
||||
Description: item.Description,
|
||||
Insured: item.Insured,
|
||||
Archived: item.Archived,
|
||||
URL: url,
|
||||
|
||||
PurchasePrice: item.PurchasePrice,
|
||||
PurchaseFrom: item.PurchaseFrom,
|
||||
@@ -219,6 +222,7 @@ func (s *IOSheet) ReadItems(ctx context.Context, items []repo.ItemOut, GID uuid.
|
||||
SoldPrice: item.SoldPrice,
|
||||
SoldNotes: item.SoldNotes,
|
||||
|
||||
Notes: item.Notes,
|
||||
Fields: customFields,
|
||||
}
|
||||
}
|
||||
@@ -252,6 +256,14 @@ func (s *IOSheet) ReadItems(ctx context.Context, items []repo.ItemOut, GID uuid.
|
||||
return nil
|
||||
}
|
||||
|
||||
func generateItemURL(item repo.ItemOut, d string) string {
|
||||
url := ""
|
||||
if item.ID != uuid.Nil {
|
||||
url = fmt.Sprintf("%s/item/%s", d, item.ID.String())
|
||||
}
|
||||
return url
|
||||
}
|
||||
|
||||
// CSV writes the current sheet to a 2d array, for compatibility with TSV/CSV files.
|
||||
func (s *IOSheet) CSV() ([][]string, error) {
|
||||
memcsv := make([][]string, len(s.Rows)+1)
|
||||
|
||||
@@ -329,7 +329,7 @@ func (svc *ItemService) CsvImport(ctx context.Context, GID uuid.UUID, data io.Re
|
||||
return finished, nil
|
||||
}
|
||||
|
||||
func (svc *ItemService) ExportCSV(ctx context.Context, GID uuid.UUID) ([][]string, error) {
|
||||
func (svc *ItemService) ExportCSV(ctx context.Context, GID uuid.UUID, hbURL string) ([][]string, error) {
|
||||
items, err := svc.repo.Items.GetAll(ctx, GID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -337,7 +337,7 @@ func (svc *ItemService) ExportCSV(ctx context.Context, GID uuid.UUID) ([][]strin
|
||||
|
||||
sheet := reporting.IOSheet{}
|
||||
|
||||
err = sheet.ReadItems(ctx, items, GID, svc.repo)
|
||||
err = sheet.ReadItems(ctx, items, GID, svc.repo, hbURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -29,9 +29,9 @@ func (tp *TemplateProps) Set(key, value string) {
|
||||
func DefaultTemplateData() TemplateProps {
|
||||
return TemplateProps{
|
||||
Defaults: TemplateDefaults{
|
||||
CompanyName: "Haybytes.com",
|
||||
CompanyName: "sysadminsmedia.com",
|
||||
CompanyAddress: "123 Main St, Anytown, CA 12345",
|
||||
CompanyURL: "https://haybytes.com",
|
||||
CompanyURL: "https://sysadminsmedia.com",
|
||||
ActivateAccountURL: "https://google.com",
|
||||
UnsubscribeURL: "https://google.com",
|
||||
},
|
||||
|
||||
@@ -6,7 +6,7 @@ export default defineConfig({
|
||||
description: "A simple home inventory management software",
|
||||
lastUpdated: true,
|
||||
sitemap: {
|
||||
hostname: 'https://homebox.sysadminsmedia.com',
|
||||
hostname: 'https://homebox.software',
|
||||
},
|
||||
|
||||
locales: {
|
||||
|
||||
@@ -1683,12 +1683,14 @@
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"example": "admin@admin.com",
|
||||
"description": "string",
|
||||
"name": "username",
|
||||
"in": "formData"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"example": "admin",
|
||||
"description": "string",
|
||||
"name": "password",
|
||||
"in": "formData"
|
||||
|
||||
@@ -47,23 +47,28 @@ type checking `task ui:check`
|
||||
|
||||
## Documentation
|
||||
We use [Vitepress](https://vitepress.dev/) for the web documentation of homebox. Anyone is welcome to contribute the documentation if they wish.
|
||||
For documentation contributions you only need NodeJS and PNPM.
|
||||
For documentation contributions, you only need Node.js and PNPM.
|
||||
|
||||
::: info Notes
|
||||
- Languages are seperated by folder (e.g `/en`, `/fr`, etc.)
|
||||
- Languages are separated by folder (e.g `/en`, `/fr`, etc.)
|
||||
- The Sidebar must be updated on a per language basis
|
||||
- Each languages files can be named independently (slugs can match the language)
|
||||
- Each language's files can be named independently (slugs can match the language)
|
||||
- The `public/_redirects` file is used to redirect the default to english
|
||||
- Redirects can also be configured per language by adding `Language=` after the redirect code
|
||||
:::
|
||||
|
||||
## Translations
|
||||
We use our own [Weblate instance](https://translate.sysadminsmedia.com/projects/homebox/) for translations. If you would like to help translate Homebox, please visit the Weblate instance and help us translate the project.
|
||||
|
||||
[](http://translate.sysadminsmedia.com/engage/homebox/)
|
||||
|
||||
## Branch Flow
|
||||
We use the `main` branch as the development branch. All PRs should be made to the `main` branch form a feature branch.
|
||||
We use the `main` branch as the development branch. All PRs should be made to the `main` branch from a feature branch.
|
||||
To create a pull request you can use the following steps:
|
||||
|
||||
1. Fork the repo and create a new branch from `main`
|
||||
2. If you added code that should be tested, add tests
|
||||
3. If you've changed APIs update the documentation
|
||||
3. If you've changed APIs, update the documentation
|
||||
4. Ensure that the test suite and linters pass
|
||||
5. Create your PR
|
||||
|
||||
|
||||
@@ -21,6 +21,8 @@ Homebox provides the option to auto-set asset IDs, this is the default behavior.
|
||||
|
||||
Example ID: `000-001`
|
||||
|
||||
To search for an Asset ID: type `#` in the search bar followed by the ID you're searching for, e.g. `#000-001`.
|
||||
|
||||
Asset IDs are partially managed by Homebox, but have a flexible implementation to allow for unique use cases. IDs are non-unique at the database level, so there is nothing stopping a user from manually setting duplicate IDs for various items. There are two recommended approaches to manage Asset IDs:
|
||||
|
||||
### 1. Auto Incrementing IDs
|
||||
|
||||
1
frontend/.npmrc
Normal file
1
frontend/.npmrc
Normal file
@@ -0,0 +1 @@
|
||||
shamefully-hoist=true
|
||||
@@ -1,9 +1,8 @@
|
||||
<template>
|
||||
<BaseModal v-model="dialog">
|
||||
<template #title> Import CSV File </template>
|
||||
<template #title> {{ $t("components.app.import_dialog.title") }} </template>
|
||||
<p>
|
||||
Import a CSV file containing your items, labels, and locations. See documentation for more information on the
|
||||
required format.
|
||||
{{ $t("components.app.import_dialog.description") }}
|
||||
</p>
|
||||
<div class="alert alert-warning shadow-lg mt-4">
|
||||
<div>
|
||||
@@ -21,8 +20,7 @@
|
||||
/>
|
||||
</svg>
|
||||
<span class="text-sm">
|
||||
Behavior for imports with existing import_refs has changed. If an import_ref is present in the CSV file, the
|
||||
item will be updated with the values in the CSV file.
|
||||
{{ $t("components.app.import_dialog.change_warning") }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -33,7 +31,7 @@
|
||||
|
||||
<BaseButton type="button" @click="uploadCsv">
|
||||
<MdiUpload class="h-5 w-5 mr-2" />
|
||||
Upload
|
||||
{{ $t("components.app.import_dialog.upload") }}
|
||||
</BaseButton>
|
||||
<p class="text-center pt-4 -mb-5">
|
||||
{{ importCsv?.name }}
|
||||
@@ -41,7 +39,7 @@
|
||||
</div>
|
||||
|
||||
<div class="modal-action">
|
||||
<BaseButton type="submit" :disabled="!importCsv"> Submit </BaseButton>
|
||||
<BaseButton type="submit" :disabled="!importCsv"> {{ $t("global.submit") }} </BaseButton>
|
||||
</div>
|
||||
</form>
|
||||
</BaseModal>
|
||||
|
||||
@@ -9,25 +9,30 @@
|
||||
{{ name != "" ? itm[name] : itm }}
|
||||
</span>
|
||||
</div>
|
||||
<ul
|
||||
<div
|
||||
tabindex="0"
|
||||
style="display: inline"
|
||||
class="dropdown-content mb-1 menu shadow border border-gray-400 rounded bg-base-100 w-full z-[9999] max-h-60 overflow-y-scroll"
|
||||
class="dropdown-content mb-1 menu w-full z-[9999] shadow border border-gray-400 rounded bg-base-100"
|
||||
>
|
||||
<div class="m-2">
|
||||
<input v-model="search" placeholder="Search…" class="input input-sm input-bordered w-full" />
|
||||
</div>
|
||||
<ul class="overflow-y-scroll max-h-60">
|
||||
<li
|
||||
v-for="(obj, idx) in items"
|
||||
v-for="(obj, idx) in filteredItems"
|
||||
:key="idx"
|
||||
:class="{
|
||||
bordered: selected[idx],
|
||||
bordered: selected.includes(obj[props.uniqueField]),
|
||||
}"
|
||||
>
|
||||
<button type="button" @click="toggle(idx)">
|
||||
<button type="button" @click="toggle(obj[props.uniqueField])">
|
||||
{{ name != "" ? obj[name] : obj }}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
@@ -49,6 +54,10 @@
|
||||
type: String,
|
||||
default: "name",
|
||||
},
|
||||
uniqueField: {
|
||||
type: String,
|
||||
default: "id",
|
||||
},
|
||||
selectFirst: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
@@ -57,19 +66,26 @@
|
||||
|
||||
const value = useVModel(props, "modelValue", emit);
|
||||
|
||||
const selected = computed<Record<number, boolean>>(() => {
|
||||
const obj: Record<number, boolean> = {};
|
||||
value.value.forEach(itm => {
|
||||
const idx = props.items.findIndex(item => item[props.name] === itm.name);
|
||||
obj[idx] = true;
|
||||
const search = ref("");
|
||||
|
||||
const filteredItems = computed(() => {
|
||||
if (!search.value) {
|
||||
return props.items;
|
||||
}
|
||||
|
||||
return props.items.filter(item => {
|
||||
return item[props.name].toLowerCase().includes(search.value.toLowerCase());
|
||||
});
|
||||
return obj;
|
||||
});
|
||||
|
||||
function toggle(index: number) {
|
||||
const item = props.items[index];
|
||||
if (selected.value[index]) {
|
||||
value.value = value.value.filter(itm => itm.name !== item.name);
|
||||
const selected = computed<string[]>(() => {
|
||||
return value.value.map(itm => itm[props.uniqueField]);
|
||||
});
|
||||
|
||||
function toggle(uniqueField: string) {
|
||||
const item = props.items.find(itm => itm[props.uniqueField] === uniqueField);
|
||||
if (selected.value.includes(item[props.uniqueField])) {
|
||||
value.value = value.value.filter(itm => itm[props.uniqueField] !== item[props.uniqueField]);
|
||||
} else {
|
||||
value.value = [...value.value, item];
|
||||
}
|
||||
|
||||
@@ -1,19 +1,24 @@
|
||||
<template>
|
||||
<BaseModal v-model="modal">
|
||||
<template #title> Create Item </template>
|
||||
<template #title> {{ $t("components.item.create_modal.title") }} </template>
|
||||
<form @submit.prevent="create()">
|
||||
<LocationSelector v-model="form.location" />
|
||||
<FormTextField ref="nameInput" v-model="form.name" :trigger-focus="focused" :autofocus="true" label="Item Name" />
|
||||
<FormTextArea v-model="form.description" label="Item Description" />
|
||||
<FormMultiselect v-model="form.labels" label="Labels" :items="labels ?? []" />
|
||||
|
||||
<div class="modal-action">
|
||||
<div class="flex justify-center">
|
||||
<div>
|
||||
<label for="photo" class="btn">{{ $t("components.item.create_modal.photo_button") }}</label>
|
||||
<input id="photo" type="file" accept="image/*" style="visibility: hidden" @change="previewImage" />
|
||||
</div>
|
||||
<BaseButton class="rounded-r-none" :loading="loading" type="submit">
|
||||
<template #icon>
|
||||
<MdiPackageVariant class="swap-off h-5 w-5" />
|
||||
<MdiPackageVariantClosed class="swap-on h-5 w-5" />
|
||||
</template>
|
||||
Create
|
||||
{{ $t("global.create") }}
|
||||
</BaseButton>
|
||||
<div class="dropdown dropdown-top">
|
||||
<label tabindex="0" class="btn rounded-l-none rounded-r-xl">
|
||||
@@ -21,12 +26,24 @@
|
||||
</label>
|
||||
<ul tabindex="0" class="dropdown-content menu p-2 shadow bg-base-100 rounded-box w-64 right-0">
|
||||
<li>
|
||||
<button type="button" @click="create(false)">Create and Add Another</button>
|
||||
<button type="button" @click="create(false)">{{ $t("global.create_and_add") }}</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- photo preview area is AFTER the create button, to avoid pushing the button below the screen on small displays -->
|
||||
<div class="border-t border-gray-300 p-4">
|
||||
<template v-if="form.preview">
|
||||
<p class="mb-0">File name: {{ form.photo?.name }}</p>
|
||||
<img
|
||||
:src="form.preview"
|
||||
class="h-[100px] w-full object-cover rounded-t shadow-sm border-gray-300"
|
||||
alt="Uploaded Photo"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</form>
|
||||
<p class="text-sm text-center mt-4">
|
||||
use <kbd class="kbd kbd-xs">Shift</kbd> + <kbd class="kbd kbd-xs"> Enter </kbd> to create and add another
|
||||
@@ -41,6 +58,7 @@
|
||||
import MdiPackageVariant from "~icons/mdi/package-variant";
|
||||
import MdiPackageVariantClosed from "~icons/mdi/package-variant-closed";
|
||||
import MdiChevronDown from "~icons/mdi/chevron-down";
|
||||
import { AttachmentTypes } from "~~/lib/api/types/non-generated";
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
@@ -85,10 +103,25 @@
|
||||
description: "",
|
||||
color: "", // Future!
|
||||
labels: [] as LabelOut[],
|
||||
preview: null as string | null,
|
||||
photo: null as File | null,
|
||||
});
|
||||
|
||||
const { shift } = useMagicKeys();
|
||||
|
||||
function previewImage(event: Event) {
|
||||
const input = event.target as HTMLInputElement;
|
||||
if (input.files && input.files.length > 0) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = e => {
|
||||
form.preview = e.target?.result as string;
|
||||
};
|
||||
const file = input.files[0];
|
||||
form.photo = file;
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
}
|
||||
|
||||
whenever(
|
||||
() => modal.value,
|
||||
() => {
|
||||
@@ -112,6 +145,13 @@
|
||||
return;
|
||||
}
|
||||
|
||||
if (loading.value) {
|
||||
toast.error("Already creating an item");
|
||||
return;
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
|
||||
if (shift.value) {
|
||||
close = false;
|
||||
}
|
||||
@@ -127,16 +167,32 @@
|
||||
const { error, data } = await api.items.create(out);
|
||||
loading.value = false;
|
||||
if (error) {
|
||||
loading.value = false;
|
||||
toast.error("Couldn't create item");
|
||||
return;
|
||||
}
|
||||
|
||||
toast.success("Item created");
|
||||
|
||||
// if the photo was provided, upload it
|
||||
if (form.photo) {
|
||||
const { error } = await api.items.attachments.add(data.id, form.photo, form.photo.name, AttachmentTypes.Photo);
|
||||
|
||||
if (error) {
|
||||
loading.value = false;
|
||||
toast.error("Failed to upload Photo");
|
||||
return;
|
||||
}
|
||||
|
||||
toast.success("Photo uploaded");
|
||||
}
|
||||
|
||||
// Reset
|
||||
form.name = "";
|
||||
form.description = "";
|
||||
form.color = "";
|
||||
form.preview = null;
|
||||
form.photo = null;
|
||||
focused.value = false;
|
||||
loading.value = false;
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
<template>
|
||||
<section>
|
||||
<BaseSectionHeader class="mb-2 flex justify-between items-center">
|
||||
Items
|
||||
{{ $t("components.item.view.selectable.items") }}
|
||||
<template #description>
|
||||
<div v-if="!viewSet" class="dropdown dropdown-hover dropdown-left">
|
||||
<label tabindex="0" class="btn btn-ghost m-1">
|
||||
@@ -39,13 +39,13 @@
|
||||
<li>
|
||||
<button @click="setViewPreference('card')">
|
||||
<MdiCardTextOutline class="h-5 w-5" />
|
||||
Card
|
||||
{{ $t("components.item.view.selectable.card") }}
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button @click="setViewPreference('table')">
|
||||
<MdiTable class="h-5 w-5" />
|
||||
Table
|
||||
{{ $t("components.item.view.selectable.table") }}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -59,7 +59,7 @@
|
||||
<template v-else>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
|
||||
<ItemCard v-for="item in items" :key="item.id" :item="item" />
|
||||
<div class="first:block hidden text-lg">No Items to Display</div>
|
||||
<div class="first:block hidden text-lg">{{ $t("components.item.view.selectable.no_items") }}</div>
|
||||
</div>
|
||||
</template>
|
||||
</section>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<BaseCard>
|
||||
<BaseCard class="overflow-clip">
|
||||
<table class="table w-full">
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -63,7 +63,14 @@
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div v-if="hasPrev || hasNext" class="border-t p-3 justify-end flex">
|
||||
<div v-if="items.length > 10" class="border-t p-3 justify-end flex gap-3">
|
||||
<div class="flex items-center">Rows per page</div>
|
||||
<select v-model.number="pagination.rowsPerPage" class="select select-sm select-primary">
|
||||
<option :value="10">10</option>
|
||||
<option :value="25">25</option>
|
||||
<option :value="50">50</option>
|
||||
<option :value="100">100</option>
|
||||
</select>
|
||||
<div class="btn-group">
|
||||
<button :disabled="!hasPrev" class="btn btn-sm" @click="prev()">«</button>
|
||||
<button class="btn btn-sm">Page {{ pagination.page }}</button>
|
||||
@@ -97,13 +104,22 @@
|
||||
] as TableHeader[];
|
||||
});
|
||||
|
||||
const preferences = useViewPreferences();
|
||||
|
||||
const pagination = reactive({
|
||||
descending: false,
|
||||
page: 1,
|
||||
rowsPerPage: 10,
|
||||
rowsPerPage: preferences.value.itemsPerTablePage,
|
||||
rowsNumber: 0,
|
||||
});
|
||||
|
||||
watch(
|
||||
() => pagination.rowsPerPage,
|
||||
newRowsPerPage => {
|
||||
preferences.value.itemsPerTablePage = newRowsPerPage;
|
||||
}
|
||||
);
|
||||
|
||||
const next = () => pagination.page++;
|
||||
const hasNext = computed<boolean>(() => {
|
||||
return pagination.page * pagination.rowsPerPage < props.items.length;
|
||||
@@ -189,4 +205,20 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
<style scoped>
|
||||
:where(.table *:first-child) :where(*:first-child) :where(th, td):first-child {
|
||||
border-top-left-radius: 0px;
|
||||
}
|
||||
|
||||
:where(.table *:first-child) :where(*:first-child) :where(th, td):last-child {
|
||||
border-top-right-radius: 0px;
|
||||
}
|
||||
|
||||
:where(.table *:last-child) :where(*:last-child) :where(th, td):first-child {
|
||||
border-bottom-left-radius: 0px;
|
||||
}
|
||||
|
||||
:where(.table *:last-child) :where(*:last-child) :where(th, td):last-child {
|
||||
border-bottom-right-radius: 0px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<BaseModal v-model="modal">
|
||||
<template #title> Create Label </template>
|
||||
<template #title>{{ $t("components.label.create_modal.title") }}</template>
|
||||
<form @submit.prevent="create()">
|
||||
<FormTextField
|
||||
ref="locationNameRef"
|
||||
@@ -12,14 +12,14 @@
|
||||
<FormTextArea v-model="form.description" label="Label Description" />
|
||||
<div class="modal-action">
|
||||
<div class="flex justify-center">
|
||||
<BaseButton class="rounded-r-none" :loading="loading" type="submit"> Create </BaseButton>
|
||||
<BaseButton class="rounded-r-none" :loading="loading" type="submit"> {{ $t("global.create") }} </BaseButton>
|
||||
<div class="dropdown dropdown-top">
|
||||
<label tabindex="0" class="btn rounded-l-none rounded-r-xl">
|
||||
<MdiChevronDown class="h-5 w-5" />
|
||||
</label>
|
||||
<ul tabindex="0" class="dropdown-content menu p-2 shadow bg-base-100 rounded-box w-64 right-0">
|
||||
<li>
|
||||
<button type="button" @click="create(false)">Create and Add Another</button>
|
||||
<button type="button" @click="create(false)">{{ $t("global.create_and_add") }}</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -71,6 +71,12 @@
|
||||
const { shift } = useMagicKeys();
|
||||
|
||||
async function create(close = true) {
|
||||
if (loading.value) {
|
||||
toast.error("Already creating a label");
|
||||
return;
|
||||
}
|
||||
loading.value = true;
|
||||
|
||||
if (shift.value) {
|
||||
close = false;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<BaseModal v-model="modal">
|
||||
<template #title> Create Location </template>
|
||||
<template #title>{{ $t("components.location.create_modal.title") }}</template>
|
||||
<form @submit.prevent="create()">
|
||||
<FormTextField
|
||||
ref="locationNameRef"
|
||||
@@ -13,14 +13,14 @@
|
||||
<LocationSelector v-model="form.parent" />
|
||||
<div class="modal-action">
|
||||
<div class="flex justify-center">
|
||||
<BaseButton class="rounded-r-none" type="submit" :loading="loading"> Create </BaseButton>
|
||||
<BaseButton class="rounded-r-none" type="submit" :loading="loading">{{ $t("global.create") }}</BaseButton>
|
||||
<div class="dropdown dropdown-top">
|
||||
<label tabindex="0" class="btn rounded-l-none rounded-r-xl">
|
||||
<MdiChevronDown class="h-5 w-5" />
|
||||
</label>
|
||||
<ul tabindex="0" class="dropdown-content menu p-2 shadow bg-base-100 rounded-box w-64 right-0">
|
||||
<li>
|
||||
<button type="button" @click="create(false)">Create and Add Another</button>
|
||||
<button type="button" @click="create(false)">{{ $t("global.create_and_add") }}</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -73,6 +73,10 @@
|
||||
const { shift } = useMagicKeys();
|
||||
|
||||
async function create(close = true) {
|
||||
if (loading.value) {
|
||||
toast.error("Already creating a location");
|
||||
return;
|
||||
}
|
||||
loading.value = true;
|
||||
|
||||
if (shift.value) {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<template>
|
||||
<BaseModal v-model="isRevealed" readonly @cancel="cancel(false)">
|
||||
<template #title> Confirm </template>
|
||||
<template #title> {{ $t("global.confirm") }} </template>
|
||||
<div>
|
||||
<p>{{ text }}</p>
|
||||
</div>
|
||||
<div class="modal-action">
|
||||
<BaseButton type="submit" @click="confirm(true)"> Confirm </BaseButton>
|
||||
<BaseButton type="submit" @click="confirm(true)"> {{ $t("global.confirm") }} </BaseButton>
|
||||
</div>
|
||||
</BaseModal>
|
||||
</template>
|
||||
|
||||
@@ -45,6 +45,7 @@
|
||||
options: any[];
|
||||
display?: string;
|
||||
modelValue: any[];
|
||||
uniqueField: string;
|
||||
};
|
||||
|
||||
const btn = ref<HTMLButtonElement>();
|
||||
@@ -75,6 +76,7 @@
|
||||
label: "",
|
||||
display: "name",
|
||||
modelValue: () => [],
|
||||
uniqueField: "id",
|
||||
});
|
||||
|
||||
const len = computed(() => {
|
||||
@@ -95,9 +97,9 @@
|
||||
const unselected = computed(() => {
|
||||
return props.options.filter(o => {
|
||||
if (searchFold.value.length > 0) {
|
||||
return o[props.display].toLowerCase().includes(searchFold.value) && !selected.value.includes(o);
|
||||
return o[props.display].toLowerCase().includes(searchFold.value) && selected.value.every(s => s[props.uniqueField] !== o[props.uniqueField]);
|
||||
}
|
||||
return !selected.value.includes(o);
|
||||
return selected.value.every(s => s[props.uniqueField] !== o[props.uniqueField]);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
</slot>
|
||||
<div tabindex="0" class="card compact dropdown-content shadow-lg bg-base-100 rounded-box w-64">
|
||||
<div class="card-body">
|
||||
<h2 class="text-center">Page URL</h2>
|
||||
<h2 class="text-center">{{ $t("components.global.page_qr_code.page_url") }}</h2>
|
||||
<img :src="getQRCodeUrl()" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="py-4">
|
||||
<p class="text-sm">Password Strength: {{ message }}</p>
|
||||
<p class="text-sm">{{ $t("components.global.password_score.password_strength") }}: {{ message }}</p>
|
||||
<progress
|
||||
class="progress w-full progress-bar"
|
||||
:value="score"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="stats bg-neutral shadow rounded-md">
|
||||
<div class="stat text-neutral-content text-center space-y-1 p-3">
|
||||
<div class="stat-title">{{ title }}</div>
|
||||
<div class="stat-title text-neutral-content">{{ title }}</div>
|
||||
<div class="stat-value text-2xl">
|
||||
<Currency v-if="type === 'currency'" :amount="value" />
|
||||
<template v-if="type === 'number'">{{ value }}</template>
|
||||
@@ -27,8 +27,4 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
[data-theme="homebox"] .stat-title {
|
||||
color: hsl(0 0% 90/0.6);
|
||||
}
|
||||
</style>
|
||||
<style></style>
|
||||
|
||||
@@ -9,6 +9,7 @@ export type LocationViewPreferences = {
|
||||
editorAdvancedView: boolean;
|
||||
itemDisplayView: ViewType;
|
||||
theme: DaisyTheme;
|
||||
itemsPerTablePage: number;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -24,6 +25,7 @@ export function useViewPreferences(): Ref<LocationViewPreferences> {
|
||||
editorAdvancedView: false,
|
||||
itemDisplayView: "card",
|
||||
theme: "homebox",
|
||||
itemsPerTablePage: 10,
|
||||
},
|
||||
{ mergeDefaults: true }
|
||||
);
|
||||
|
||||
@@ -32,7 +32,7 @@ export function useRouteQuery(q: string, def: any): WritableComputedRef<any> {
|
||||
case "string":
|
||||
return computed({
|
||||
get: () => {
|
||||
const qv = route.query[q];
|
||||
const qv = first.value;
|
||||
if (Array.isArray(qv)) {
|
||||
return qv[0];
|
||||
}
|
||||
|
||||
@@ -29,6 +29,12 @@
|
||||
</div>
|
||||
|
||||
<slot></slot>
|
||||
<footer v-if="status" class="text-center w-full bottom-0 pb-4 bg-base-300 text-secondary-content">
|
||||
<p class="text-center text-sm">
|
||||
{{ $t("global.version", { version: status.build.version }) }} ~
|
||||
{{ $t("global.build", { build: status.build.commit }) }}
|
||||
</p>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<!-- Sidebar -->
|
||||
@@ -39,7 +45,7 @@
|
||||
<div class="w-60 py-5 md:py-10 bg-base-200 flex flex-grow-1 flex-col">
|
||||
<div class="space-y-8">
|
||||
<div class="flex flex-col items-center gap-4">
|
||||
<p>Welcome, {{ username }}</p>
|
||||
<p>{{ $t("global.welcome", { username: username }) }}</p>
|
||||
<NuxtLink class="avatar placeholder" to="/home">
|
||||
<div class="bg-base-300 text-neutral-content rounded-full w-24 p-4">
|
||||
<AppLogo />
|
||||
@@ -53,7 +59,7 @@
|
||||
<span>
|
||||
<MdiPlus class="mr-1 -ml-1" />
|
||||
</span>
|
||||
Create
|
||||
{{ $t("global.create") }}
|
||||
</label>
|
||||
<ul tabindex="0" class="dropdown-content menu p-2 shadow bg-base-100 rounded-box w-40">
|
||||
<li v-for="btn in dropdown" :key="btn.name">
|
||||
@@ -83,7 +89,9 @@
|
||||
</div>
|
||||
|
||||
<!-- Bottom -->
|
||||
<button class="mt-auto mx-2 hover:bg-base-300 p-3 rounded-btn" @click="logout">Sign Out</button>
|
||||
<button class="mt-auto mx-2 hover:bg-base-300 p-3 rounded-btn" @click="logout">
|
||||
{{ $t("global.sign_out") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -101,12 +109,17 @@
|
||||
import MdiMagnify from "~icons/mdi/magnify";
|
||||
import MdiAccount from "~icons/mdi/account";
|
||||
import MdiCog from "~icons/mdi/cog";
|
||||
|
||||
const username = computed(() => authCtx.user?.name || "User");
|
||||
|
||||
const pubApi = usePublicApi();
|
||||
const { data: status } = useAsyncData(async () => {
|
||||
const { data } = await pubApi.status();
|
||||
|
||||
return data;
|
||||
});
|
||||
|
||||
// Preload currency format
|
||||
useFormatCurrency();
|
||||
|
||||
const modals = reactive({
|
||||
item: false,
|
||||
location: false,
|
||||
|
||||
80
frontend/locales/ca.json
Normal file
80
frontend/locales/ca.json
Normal file
@@ -0,0 +1,80 @@
|
||||
{
|
||||
"components": {
|
||||
"app": {
|
||||
"import_dialog": {
|
||||
"title": "Importa un fitxer CSV",
|
||||
"upload": "Puja",
|
||||
"description": "Importa un fitxer CSV que contingui els articles, etiquetes i ubicacions. \nConsulteu la documentació per a més informació sobre el format requerit.",
|
||||
"change_warning": "El comportament de les importacions amb import_refs existents ha canviat. Si hi ha un import_refs al fitxer CSV, \nl'article s'actualitzarà amb els valors del fitxer CSV."
|
||||
}
|
||||
},
|
||||
"item": {
|
||||
"create_modal": {
|
||||
"title": "Crea un article",
|
||||
"photo_button": "Foto 📷"
|
||||
},
|
||||
"view": {
|
||||
"selectable": {
|
||||
"card": "Targeta",
|
||||
"items": "Articles",
|
||||
"no_items": "No hi ha articles a mostrar",
|
||||
"table": "Taula"
|
||||
}
|
||||
}
|
||||
},
|
||||
"label": {
|
||||
"create_modal": {
|
||||
"title": "Crea una etiqueta"
|
||||
}
|
||||
},
|
||||
"location": {
|
||||
"create_modal": {
|
||||
"title": "Crea una ubicació"
|
||||
}
|
||||
},
|
||||
"global": {
|
||||
"page_qr_code": {
|
||||
"page_url": "URL de la pàgina"
|
||||
},
|
||||
"password_score": {
|
||||
"password_strength": "Força de la contrasenya"
|
||||
}
|
||||
}
|
||||
},
|
||||
"global": {
|
||||
"build": "Construcció: { build }",
|
||||
"confirm": "Confirma",
|
||||
"create": "Crea",
|
||||
"create_and_add": "Crea i afegeix-ne un altre",
|
||||
"created": "Creat",
|
||||
"email": "Correu electrònic",
|
||||
"follow_dev": "Segueix al desenvolupador",
|
||||
"github": "Projecte de GitHub",
|
||||
"items": "Articles",
|
||||
"join_discord": "Uniu-vos a Discord",
|
||||
"locations": "Ubicacions",
|
||||
"name": "Nom",
|
||||
"read_docs": "Llegiu la documentació",
|
||||
"search": "Cerca",
|
||||
"sign_out": "Tanca la sessió",
|
||||
"submit": "Envia",
|
||||
"welcome": "Us donem la benvinguda, { username }",
|
||||
"labels": "Etiquetes",
|
||||
"password": "Contrasenya",
|
||||
"version": "Versió {version}"
|
||||
},
|
||||
"index": {
|
||||
"joining_group": "Us uniu a un grup existent!",
|
||||
"dont_join_group": "Voleu unir-vos al grup?",
|
||||
"login": "Inici de sessió",
|
||||
"disabled_registration": "El registre és desactivat",
|
||||
"register": "Registra-m'hi",
|
||||
"remember_me": "Recorda'm",
|
||||
"set_email": "Quin és el seu correu electrònic?",
|
||||
"set_name": "Com us dieu?",
|
||||
"set_password": "Definiu la contrasenya"
|
||||
},
|
||||
"items": {
|
||||
"negate_labels": "Nega les etiquetes seleccionades"
|
||||
}
|
||||
}
|
||||
128
frontend/locales/de.json
Normal file
128
frontend/locales/de.json
Normal file
@@ -0,0 +1,128 @@
|
||||
{
|
||||
"components": {
|
||||
"app": {
|
||||
"import_dialog": {
|
||||
"change_warning": "Das Verhalten beim Importieren vorhandener import_refs hat sich geändert. Wenn ein import_ref in der CSV-Datei vorhanden ist, \nwird der Gegenstand mit den Werten in der CSV-Datei aktualisiert.",
|
||||
"description": "Importiere eine CSV-Datei, die deine Gegenstände, Etiketten und Standorte enthält. Schau in die Dokumentation für weitere Informationen\nzum erforderlichen Format.",
|
||||
"title": "CSV-Datei importieren",
|
||||
"upload": "Hochladen"
|
||||
}
|
||||
},
|
||||
"global": {
|
||||
"page_qr_code": {
|
||||
"page_url": "Seiten-URL"
|
||||
},
|
||||
"password_score": {
|
||||
"password_strength": "Passwortstärke"
|
||||
}
|
||||
},
|
||||
"item": {
|
||||
"create_modal": {
|
||||
"title": "Gegenstand erstellen",
|
||||
"photo_button": "Foto 📷"
|
||||
},
|
||||
"view": {
|
||||
"selectable": {
|
||||
"card": "Karte",
|
||||
"items": "Gegenstände",
|
||||
"no_items": "Keine Gegenstände anzuzeigen",
|
||||
"table": "Tabelle"
|
||||
}
|
||||
}
|
||||
},
|
||||
"label": {
|
||||
"create_modal": {
|
||||
"title": "Etikett erstellen"
|
||||
}
|
||||
},
|
||||
"location": {
|
||||
"create_modal": {
|
||||
"title": "Standort erstellen"
|
||||
}
|
||||
}
|
||||
},
|
||||
"global": {
|
||||
"build": "Build: { build }",
|
||||
"confirm": "Bestätigen",
|
||||
"create": "Erstellen",
|
||||
"create_and_add": "Erstellen und weiteren hinzufügen",
|
||||
"created": "Erstellt",
|
||||
"email": "E-Mail",
|
||||
"follow_dev": "Dem Entwickler folgen",
|
||||
"github": "GitHub-Projekt",
|
||||
"items": "Gegenstände",
|
||||
"join_discord": "Discord beitreten",
|
||||
"labels": "Etiketten",
|
||||
"locations": "Lagerorte",
|
||||
"name": "Name",
|
||||
"password": "Passwort",
|
||||
"read_docs": "Dokumentation lesen",
|
||||
"search": "Suche",
|
||||
"sign_out": "Abmelden",
|
||||
"submit": "Einreichen",
|
||||
"version": "Version: { version }",
|
||||
"welcome": "Willkommen, { username }"
|
||||
},
|
||||
"index": {
|
||||
"disabled_registration": "Registrierung deaktiviert",
|
||||
"dont_join_group": "Möchtest du nicht einer Gruppe beitreten?",
|
||||
"joining_group": "Du trittst einer bereits bestehenden Gruppe bei!",
|
||||
"login": "Anmelden",
|
||||
"register": "Registrieren",
|
||||
"remember_me": "Angemeldet bleiben",
|
||||
"set_email": "Was ist deine E-Mail?",
|
||||
"set_name": "Wie heißt du?",
|
||||
"set_password": "Setze dein Passwort",
|
||||
"tagline": "Verfolgen, Organisieren und Verwalten deiner Sachen."
|
||||
},
|
||||
"items": {
|
||||
"add": "Hinzufügen",
|
||||
"created_at": "Erstellt am",
|
||||
"custom_fields": "Benutzerdefinierte Felder",
|
||||
"field_selector": "Feldauswahl",
|
||||
"field_value": "Feldwert",
|
||||
"first": "Erste",
|
||||
"include_archive": "Archivierte Elemente einschließen",
|
||||
"last": "Letzte",
|
||||
"negate_labels": "Ausgewählte Etiketten negieren",
|
||||
"next_page": "Nächste Seite",
|
||||
"no_results": "Keine Elemente gefunden",
|
||||
"options": "Optionen",
|
||||
"order_by": "Sortieren nach",
|
||||
"pages": "Seite { page } von { totalPages }",
|
||||
"prev_page": "Vorherige Seite",
|
||||
"query_id": "Abfrage Asset-ID-Nummer: { id }",
|
||||
"reset_search": "Suche zurücksetzen",
|
||||
"results": "{ total } Ergebnisse",
|
||||
"tip_1": "Standort- und Etikettenfilter verwenden die 'ODER'-Operation. Wenn mehr als eines ausgewählt ist, wird\n nur eines für eine Übereinstimmung benötigt.",
|
||||
"tip_2": "Suchen, die mit '#' beginnen, fragen nach einer Asset-ID (Beispiel '#000-001')",
|
||||
"tip_3": "Feldfilter verwenden die 'ODER'-Operation. Wenn mehr als eines ausgewählt ist, wird nur eines\n für eine Übereinstimmung benötigt.",
|
||||
"tips": "Tipps",
|
||||
"tips_sub": "Suchtipps",
|
||||
"updated_at": "Aktualisiert am"
|
||||
},
|
||||
"profile": {
|
||||
"active": "Aktiv",
|
||||
"change_password": "Passwort ändern",
|
||||
"currency_format": "Währungsformat",
|
||||
"current_password": "Aktuelles Passwort",
|
||||
"delete_account": "Konto löschen",
|
||||
"delete_account_sub": "Lösche dein Konto und alle zugehörigen Daten. Dies kann nicht rückgängig gemacht werden.",
|
||||
"enabled": "Aktiviert",
|
||||
"gen_invite": "Einladungslink generieren",
|
||||
"group_settings": "Gruppeneinstellungen",
|
||||
"group_settings_sub": "Geteilte Gruppeneinstellungen. Möglicherweise musst du die Seite neu laden, damit einige Einstellungen wirksam werden.",
|
||||
"inactive": "Inaktiv",
|
||||
"new_password": "Neues Passwort",
|
||||
"notifier_modal": "{ type, select, true {Bearbeiten} false {Erstellen} other {Andere}} Notifier",
|
||||
"notifiers": "Melder",
|
||||
"notifiers_sub": "Erhalte Benachrichtigungen über bevorstehende Wartungserinnerungen",
|
||||
"test": "Test",
|
||||
"theme_settings": "Themes",
|
||||
"theme_settings_sub": "Theme-Einstellungen werden im lokalen Speicher deines Browsers gespeichert. Du kannst das Theme jederzeit ändern. Wenn du Probleme hast, dein Theme einzustellen, versuche, die Seite neu zu laden.",
|
||||
"update_group": "Gruppe aktualisieren",
|
||||
"url": "URL",
|
||||
"user_profile": "Benutzerprofil",
|
||||
"user_profile_sub": "Lade Benutzer ein und verwalte dein Konto."
|
||||
}
|
||||
}
|
||||
128
frontend/locales/en.json
Normal file
128
frontend/locales/en.json
Normal file
@@ -0,0 +1,128 @@
|
||||
{
|
||||
"components": {
|
||||
"app": {
|
||||
"import_dialog": {
|
||||
"change_warning": "Behavior for imports with existing import_refs has changed. If an import_ref is present in the CSV file, the \nitem will be updated with the values in the CSV file.",
|
||||
"description": "Import a CSV file containing your items, labels, and locations. See documentation for more information on the \nrequired format.",
|
||||
"title": "Import CSV File",
|
||||
"upload": "Upload"
|
||||
}
|
||||
},
|
||||
"global": {
|
||||
"page_qr_code": {
|
||||
"page_url": "Page URL"
|
||||
},
|
||||
"password_score": {
|
||||
"password_strength": "Password Strength"
|
||||
}
|
||||
},
|
||||
"item": {
|
||||
"create_modal": {
|
||||
"title": "Create Item",
|
||||
"photo_button": "Photo \uD83D\uDCF7"
|
||||
},
|
||||
"view": {
|
||||
"selectable": {
|
||||
"card": "Card",
|
||||
"items": "Items",
|
||||
"no_items": "No Items to Display",
|
||||
"table": "Table"
|
||||
}
|
||||
}
|
||||
},
|
||||
"label": {
|
||||
"create_modal": {
|
||||
"title": "Create Label"
|
||||
}
|
||||
},
|
||||
"location": {
|
||||
"create_modal": {
|
||||
"title": "Create Location"
|
||||
}
|
||||
}
|
||||
},
|
||||
"global": {
|
||||
"build": "Build: { build }",
|
||||
"confirm": "Confirm",
|
||||
"create": "Create",
|
||||
"create_and_add": "Create and Add Another",
|
||||
"created": "Created",
|
||||
"email": "Email",
|
||||
"follow_dev": "Follow the Developer",
|
||||
"github": "GitHub Project",
|
||||
"items": "Items",
|
||||
"join_discord": "Join the Discord",
|
||||
"labels": "Labels",
|
||||
"locations": "Locations",
|
||||
"name": "Name",
|
||||
"password": "Password",
|
||||
"read_docs": "Read the Docs",
|
||||
"search": "Search",
|
||||
"sign_out": "Sign Out",
|
||||
"submit": "Submit",
|
||||
"version": "Version: { version }",
|
||||
"welcome": "Welcome, { username }"
|
||||
},
|
||||
"index": {
|
||||
"disabled_registration": "Registration Disabled",
|
||||
"dont_join_group": "Don't want to join a group?",
|
||||
"joining_group": "You're Joining an Existing Group!",
|
||||
"login": "Login",
|
||||
"register": "Register",
|
||||
"remember_me": "Remember Me",
|
||||
"set_email": "What's your email?",
|
||||
"set_name": "What's your name?",
|
||||
"set_password": "Set your password",
|
||||
"tagline": "Track, Organize, and Manage your Things."
|
||||
},
|
||||
"items": {
|
||||
"add": "Add",
|
||||
"created_at": "Created At",
|
||||
"custom_fields": "Custom Fields",
|
||||
"field_selector": "Field Selector",
|
||||
"field_value": "Field Value",
|
||||
"first": "First",
|
||||
"include_archive": "Include Archived Items",
|
||||
"last": "Last",
|
||||
"negate_labels": "Negate Selected Labels",
|
||||
"next_page": "Next Page",
|
||||
"no_results": "No Items Found",
|
||||
"options": "Options",
|
||||
"order_by": "Order By",
|
||||
"pages": "Page { page } of { totalPages }",
|
||||
"prev_page": "Previous Page",
|
||||
"query_id": "Querying Asset ID Number: { id }",
|
||||
"reset_search": "Reset Search",
|
||||
"results": "{ total } Results",
|
||||
"tip_1": "Location and label filters use the 'OR' operation. If more than one is selected only one will be\n required for a match.",
|
||||
"tip_2": "Searches prefixed with '#'' will query for a asset ID (example '#000-001')",
|
||||
"tip_3": "Field filters use the 'OR' operation. If more than one is selected only one will be required for a\n match.",
|
||||
"tips": "Tips",
|
||||
"tips_sub": "Search Tips",
|
||||
"updated_at": "Updated At"
|
||||
},
|
||||
"profile": {
|
||||
"active": "Active",
|
||||
"change_password": "Change Password",
|
||||
"currency_format": "Currency Format",
|
||||
"current_password": "Current Password",
|
||||
"delete_account": "Delete Account",
|
||||
"delete_account_sub": "Delete your account and all its associated data. This can not be undone.",
|
||||
"enabled": "Enabled",
|
||||
"gen_invite": "Generate Invite Link",
|
||||
"group_settings": "Group Settings",
|
||||
"group_settings_sub": "Shared Group Settings. You may need to refresh your browser for some settings to apply.",
|
||||
"inactive": "Inactive",
|
||||
"new_password": "New Password",
|
||||
"notifier_modal": "{ type, select, true {Edit} false {Create} other {Other}} Notifier",
|
||||
"notifiers": "Notifiers",
|
||||
"notifiers_sub": "Get notifications for upcoming maintenance reminders",
|
||||
"test": "Test",
|
||||
"theme_settings": "Theme Settings",
|
||||
"theme_settings_sub": "Theme settings are stored in your browser's local storage. You can change the theme at any time. If you're\n having trouble setting your theme try refreshing your browser.",
|
||||
"update_group": "Update Group",
|
||||
"url": "URL",
|
||||
"user_profile": "User Profile",
|
||||
"user_profile_sub": "Invite users, and manage your account."
|
||||
}
|
||||
}
|
||||
128
frontend/locales/es.json
Normal file
128
frontend/locales/es.json
Normal file
@@ -0,0 +1,128 @@
|
||||
{
|
||||
"components": {
|
||||
"app": {
|
||||
"import_dialog": {
|
||||
"change_warning": "Se ha modificado el comportamiento de las importaciones con import_refs existentes. Si existe una import_ref en el archivo CSV, el \nelemento se actualizará con los valores del archivo CSV.",
|
||||
"description": "Importa un archivo CSV que contenga tus elementos, etiquetas y ubicaciones. Consulta la documentación para obtener más información sobre el \nformato requerido.",
|
||||
"title": "Importar Archivo CSV",
|
||||
"upload": "Subir"
|
||||
}
|
||||
},
|
||||
"global": {
|
||||
"page_qr_code": {
|
||||
"page_url": "URL de página"
|
||||
},
|
||||
"password_score": {
|
||||
"password_strength": "Seguridad de la contraseña"
|
||||
}
|
||||
},
|
||||
"item": {
|
||||
"create_modal": {
|
||||
"title": "Crear Elemento",
|
||||
"photo_button": "Foto 📷"
|
||||
},
|
||||
"view": {
|
||||
"selectable": {
|
||||
"card": "Tarjeta",
|
||||
"items": "Elementos",
|
||||
"no_items": "No hay elementos para mostrar",
|
||||
"table": "Tabla"
|
||||
}
|
||||
}
|
||||
},
|
||||
"label": {
|
||||
"create_modal": {
|
||||
"title": "Crear Etiqueta"
|
||||
}
|
||||
},
|
||||
"location": {
|
||||
"create_modal": {
|
||||
"title": "Crear Ubicación"
|
||||
}
|
||||
}
|
||||
},
|
||||
"global": {
|
||||
"build": "Compilación: { build }",
|
||||
"confirm": "Confirmar",
|
||||
"create": "Crear",
|
||||
"created": "Creado",
|
||||
"email": "Email",
|
||||
"github": "Proyecto GitHub",
|
||||
"items": "Elementos",
|
||||
"join_discord": "Únete al Discord",
|
||||
"labels": "Etiquetas",
|
||||
"locations": "Ubicaciones",
|
||||
"name": "Nombre",
|
||||
"password": "Contraseña",
|
||||
"read_docs": "Lee la Documentación",
|
||||
"search": "Buscar",
|
||||
"sign_out": "Cerrar Sesión",
|
||||
"submit": "Enviar",
|
||||
"welcome": "Bienvenido/a, { username }",
|
||||
"create_and_add": "Crear y Añadir Otro",
|
||||
"follow_dev": "Seguir al Desarrollador",
|
||||
"version": "Versión: { version }"
|
||||
},
|
||||
"index": {
|
||||
"disabled_registration": "Registro Desactivado",
|
||||
"dont_join_group": "¿No quieres unirte a un grupo?",
|
||||
"login": "Iniciar sesión",
|
||||
"register": "Registrarse",
|
||||
"remember_me": "Recuérdame",
|
||||
"set_email": "¿Cuál es tu email?",
|
||||
"set_name": "¿Cómo te llamas?",
|
||||
"set_password": "Establece tu contraseña",
|
||||
"tagline": "Rastrea, Organiza y Gestiona tus Cosas.",
|
||||
"joining_group": "¡Te estás uniendo a un grupo existente!"
|
||||
},
|
||||
"items": {
|
||||
"add": "Añadir",
|
||||
"created_at": "Creado El",
|
||||
"custom_fields": "Campos Personalizados",
|
||||
"field_selector": "Selector de Campo",
|
||||
"field_value": "Valor del Campo",
|
||||
"first": "Primer",
|
||||
"include_archive": "Incluir Elementos Archivados",
|
||||
"last": "Último",
|
||||
"negate_labels": "Negar Etiquetas Seleccionadas",
|
||||
"next_page": "Siguiente Página",
|
||||
"no_results": "No se Encontraron Elementos",
|
||||
"options": "Opciones",
|
||||
"order_by": "Ordenar Por",
|
||||
"pages": "Página { page } de { totalPages }",
|
||||
"prev_page": "Anterior Página",
|
||||
"query_id": "Consultar Número ID del Activo: { id }",
|
||||
"reset_search": "Restablecer Búsqueda",
|
||||
"results": "{ total } Resultados",
|
||||
"tip_2": "Las búsquedas precedidas de \"#\" buscarán un ID de activo (por ejemplo, \"#000-001\")",
|
||||
"tip_3": "Los filtros de campo utilizan el operador \"OR\". Si se selecciona más de uno, sólo se requerirá uno para una\n coincidencia.",
|
||||
"tip_1": "Los filtros de ubicación y etiquetas utilizan el operador \"OR\". Si se selecciona más de uno, sólo uno será\n necesario para obtener una coincidencia.",
|
||||
"tips": "Sugerencias",
|
||||
"tips_sub": "Sugerencias de Búsqueda",
|
||||
"updated_at": "Actualizado El"
|
||||
},
|
||||
"profile": {
|
||||
"active": "Activo",
|
||||
"change_password": "Cambiar Contraseña",
|
||||
"currency_format": "Formato Divisa",
|
||||
"current_password": "Contraseña Actual",
|
||||
"delete_account": "Eliminar Cuenta",
|
||||
"enabled": "Habilitado",
|
||||
"gen_invite": "Generar Enlace de Invitación",
|
||||
"group_settings": "Ajustes de Grupo",
|
||||
"group_settings_sub": "Configuración de Grupo Compartido. Es posible que tengas que actualizar tu navegador para que se apliquen algunos ajustes.",
|
||||
"inactive": "Inactivo",
|
||||
"new_password": "Nueva Contraseña",
|
||||
"notifiers": "Notificaciones",
|
||||
"notifiers_sub": "Recibe notificaciones de los próximos recordatorios de mantenimiento",
|
||||
"test": "Probar",
|
||||
"theme_settings": "Ajustes de Tema",
|
||||
"update_group": "Actualizar Grupo",
|
||||
"url": "URL",
|
||||
"user_profile": "Perfil de Usuario",
|
||||
"user_profile_sub": "Invita a usuarios y gestiona tu cuenta.",
|
||||
"delete_account_sub": "Elimina tu cuenta y todos sus datos asociados. Esto no se puede deshacer.",
|
||||
"notifier_modal": "{ type, select, true {Edit} false {Create} other {Other}} Notificación",
|
||||
"theme_settings_sub": "La configuración del tema se guarda en el almacenamiento local de tu navegador. Puedes cambiar el tema en cualquier momento. Si tienes\n problemas para configurar el tema, prueba a actualizar el navegador."
|
||||
}
|
||||
}
|
||||
128
frontend/locales/fr.json
Normal file
128
frontend/locales/fr.json
Normal file
@@ -0,0 +1,128 @@
|
||||
{
|
||||
"global": {
|
||||
"email": "Courriel",
|
||||
"read_docs": "Lire la documentation",
|
||||
"password": "Mot de passe",
|
||||
"github": "Projet GitHub",
|
||||
"join_discord": "Rejoindre le Discord",
|
||||
"follow_dev": "Suivre le développeur",
|
||||
"version": "Version : { version }",
|
||||
"build": "Assemblage : { build }",
|
||||
"create": "Créer",
|
||||
"submit": "Soumettre",
|
||||
"create_and_add": "Créer et en ajouter un autre",
|
||||
"confirm": "Confirmer",
|
||||
"sign_out": "Déconnexion",
|
||||
"welcome": "Bienvenue, { username }",
|
||||
"created": "Créé",
|
||||
"labels": "Étiquettes",
|
||||
"locations": "Emplacements",
|
||||
"name": "Nom",
|
||||
"search": "Rechercher",
|
||||
"items": "Articles"
|
||||
},
|
||||
"index": {
|
||||
"set_email": "Quel est vôtre courriel ?",
|
||||
"set_name": "Quel est votre nom ?",
|
||||
"login": "Se connecter",
|
||||
"register": "S'enregistrer",
|
||||
"remember_me": "Se souvenir de moi",
|
||||
"set_password": "Définir votre mot de passe",
|
||||
"dont_join_group": "Vous ne voulez pas joindre un groupe ?",
|
||||
"tagline": "Répertoriez, organisez et gérez vos affaires.",
|
||||
"disabled_registration": "Les inscriptions sont désactivées",
|
||||
"joining_group": "Vous rejoignez un groupe existant !"
|
||||
},
|
||||
"components": {
|
||||
"label": {
|
||||
"create_modal": {
|
||||
"title": "Créer une étiquette"
|
||||
}
|
||||
},
|
||||
"item": {
|
||||
"view": {
|
||||
"selectable": {
|
||||
"items": "Articles",
|
||||
"no_items": "Nb. d’articles à afficher",
|
||||
"table": "Tableau",
|
||||
"card": "Carte"
|
||||
}
|
||||
},
|
||||
"create_modal": {
|
||||
"title": "Créer un article",
|
||||
"photo_button": "Photo 📷"
|
||||
}
|
||||
},
|
||||
"location": {
|
||||
"create_modal": {
|
||||
"title": "Créer un emplacement"
|
||||
}
|
||||
},
|
||||
"global": {
|
||||
"password_score": {
|
||||
"password_strength": "Solidité du mot de passe"
|
||||
},
|
||||
"page_qr_code": {
|
||||
"page_url": "URL de la page"
|
||||
}
|
||||
},
|
||||
"app": {
|
||||
"import_dialog": {
|
||||
"title": "Importer un fichier CSV",
|
||||
"upload": "Téléverser",
|
||||
"description": "Importer un fichier CSV contenant vos articles, étiquettes, et emplacements. Voir la documentation pour plus d’informations sur le format requis.",
|
||||
"change_warning": "Le comportement lors d’importations avec des import_ref existants a changé. Si une valeur pour import_ref est présente dans le fichier CSV, alors l’élément sera mis à jour avec celle-ci."
|
||||
}
|
||||
}
|
||||
},
|
||||
"profile": {
|
||||
"notifiers_sub": "Recevez des notifications pour vous prévenir des prochaines maintenances.",
|
||||
"active": "Actif",
|
||||
"enabled": "Activé",
|
||||
"test": "Test",
|
||||
"current_password": "Mot de passe actuel",
|
||||
"new_password": "Nouveau mot de passe",
|
||||
"change_password": "Changer de mot de passe",
|
||||
"inactive": "Inactif",
|
||||
"notifiers": "Notifications",
|
||||
"gen_invite": "Générer un lien d’invitation",
|
||||
"user_profile": "Profil utilisateur",
|
||||
"user_profile_sub": "Gérez votre compte et invitez des utilisateurs.",
|
||||
"url": "URL",
|
||||
"delete_account": "Effacer le compte",
|
||||
"delete_account_sub": "Supprimer le compte et toutes ses données. Aucune récupération possible.",
|
||||
"group_settings": "Paramètres du groupe",
|
||||
"group_settings_sub": "Paramètres du groupe partagé. Il peut être nécessaire de recharger la page pour qu’ils deviennent effectifs.",
|
||||
"currency_format": "Format de la devise",
|
||||
"update_group": "Mettre à jour le groupe",
|
||||
"theme_settings": "Paramètres du thème",
|
||||
"theme_settings_sub": "Les paramètres du thème sont stockés dans le navigateur. Vous pouvez les changer à tout moment. Si vous\nrencontrez des problèmes, il est conseillé de rafraichir la page.",
|
||||
"notifier_modal": "Notifications { type, select, true {Edit} false {Create} other {Other}}"
|
||||
},
|
||||
"items": {
|
||||
"add": "Ajouter",
|
||||
"created_at": "Créé à",
|
||||
"custom_fields": "Champs personnalisés",
|
||||
"field_selector": "Sélecteur de champ",
|
||||
"field_value": "Valeur du champ",
|
||||
"first": "Premier",
|
||||
"include_archive": "Inclure les éléments archivés",
|
||||
"last": "Dernier",
|
||||
"negate_labels": "Négliger les étiquettes sélectionnées",
|
||||
"next_page": "Page suivante",
|
||||
"no_results": "Aucun élément trouvé",
|
||||
"options": "Options",
|
||||
"order_by": "Trier par",
|
||||
"pages": "Page { page } sur { totalPages }",
|
||||
"prev_page": "Page précédente",
|
||||
"query_id": "Interrogation du numéro d'identification de l'actif : { id }",
|
||||
"reset_search": "Réinitialiser la recherche",
|
||||
"tip_1": "Les filtres d’emplacement et d’étiquette utilisent l’opérateur « OU ».\nUn seul des filtres n’a besoin de correspondre pour retourner un résultat.",
|
||||
"tip_2": "Les recherches préfixées par '#'' rechercheront un ID d'actif (exemple '#000-001')",
|
||||
"tip_3": "Les filtres de champ utilisent l’opérateur « OU ».\nUn seul des filtres n’a besoin de correspondre pour retourner un résultat.",
|
||||
"tips": "Conseils",
|
||||
"tips_sub": "Conseils pour la recherche",
|
||||
"updated_at": "Mis à jour le",
|
||||
"results": "{ total } résultats"
|
||||
}
|
||||
}
|
||||
127
frontend/locales/it.json
Normal file
127
frontend/locales/it.json
Normal file
@@ -0,0 +1,127 @@
|
||||
{
|
||||
"components": {
|
||||
"app": {
|
||||
"import_dialog": {
|
||||
"change_warning": "Il comportamento per le importazioni con import_ref esistenti è cambiato. Se un import_ref è presente nel file CSV,\n l'elemento verrà aggiornato con i valori presenti nel file CSV.",
|
||||
"description": "Importa un file CSV contenente gli oggetti, le etichette e le posizioni. Vedi la documentazione per ulteriori informazioni sul \nformato richiesto.",
|
||||
"title": "Importa File CSV",
|
||||
"upload": "Carica"
|
||||
}
|
||||
},
|
||||
"global": {
|
||||
"page_qr_code": {
|
||||
"page_url": "URL della Pagina"
|
||||
},
|
||||
"password_score": {
|
||||
"password_strength": "Complessità della Password"
|
||||
}
|
||||
},
|
||||
"item": {
|
||||
"create_modal": {
|
||||
"title": "Crea Oggetto"
|
||||
},
|
||||
"view": {
|
||||
"selectable": {
|
||||
"card": "Scheda",
|
||||
"items": "Oggetti",
|
||||
"no_items": "Nessun Oggetto da Visualizzare",
|
||||
"table": "Tabella"
|
||||
}
|
||||
}
|
||||
},
|
||||
"label": {
|
||||
"create_modal": {
|
||||
"title": "Crea Etichetta"
|
||||
}
|
||||
},
|
||||
"location": {
|
||||
"create_modal": {
|
||||
"title": "Crea Posizione"
|
||||
}
|
||||
}
|
||||
},
|
||||
"global": {
|
||||
"build": "Build: { build }",
|
||||
"confirm": "Conferma",
|
||||
"create": "Crea",
|
||||
"create_and_add": "Crea e aggiungi un altro",
|
||||
"created": "Creato",
|
||||
"email": "Email",
|
||||
"follow_dev": "Segui lo Sviluppatore",
|
||||
"github": "Progetto GitHub",
|
||||
"items": "Articoli",
|
||||
"join_discord": "Unisciti a Discord",
|
||||
"labels": "Etichette",
|
||||
"locations": "Posizioni",
|
||||
"name": "Nome",
|
||||
"password": "Password",
|
||||
"read_docs": "Leggi la Documentazione",
|
||||
"search": "Cerca",
|
||||
"sign_out": "Disconnetti",
|
||||
"submit": "Invia",
|
||||
"version": "Versione: { version }",
|
||||
"welcome": "Benvenuto, { username }"
|
||||
},
|
||||
"index": {
|
||||
"disabled_registration": "Registrazione Disabilitata",
|
||||
"dont_join_group": "Non vuoi unirti a un gruppo?",
|
||||
"joining_group": "Stai unendoti a un gruppo esistente!",
|
||||
"login": "Accedi",
|
||||
"register": "Registrati",
|
||||
"remember_me": "Ricordami",
|
||||
"set_email": "Qual è la tua email?",
|
||||
"set_name": "Come ti chiami?",
|
||||
"set_password": "Imposta la tua password",
|
||||
"tagline": "Tieni traccia, Organizza e Gestisci le tue Cose."
|
||||
},
|
||||
"items": {
|
||||
"add": "Aggiungi",
|
||||
"created_at": "Creato Il",
|
||||
"custom_fields": "Campi Personalizzati",
|
||||
"field_selector": "Selettore di Campo",
|
||||
"field_value": "Valore del Campo",
|
||||
"first": "Primo",
|
||||
"include_archive": "Includi Articoli Archiviati",
|
||||
"last": "Ultimo",
|
||||
"negate_labels": "Negare Etichette Selezionate",
|
||||
"next_page": "Pagina Successiva",
|
||||
"no_results": "Nessun Articolo Trovato",
|
||||
"options": "Opzioni",
|
||||
"order_by": "Ordina Per",
|
||||
"pages": "Pagina { page } di { totalPages }",
|
||||
"prev_page": "Pagina Precedente",
|
||||
"query_id": "ID dell'Asset in Ricerca: { id }",
|
||||
"reset_search": "Reimposta Ricerca",
|
||||
"results": "{ total } Risultati",
|
||||
"tip_1": "I filtri di posizione ed etichetta utilizzano l'operazione 'OR'. Se ne viene selezionato più\n di uno, ne sarà richiesto solo uno per una corrispondenza.",
|
||||
"tip_2": "Le ricerche con prefisso '#' cercheranno un ID asset (esempio '#000-001')",
|
||||
"tip_3": "I filtri di campo utilizzano l'operazione 'OR'. Se ne viene selezionato più di uno, ne sarà\n richiesto solo uno per una corrispondenza.",
|
||||
"tips": "Suggerimenti",
|
||||
"tips_sub": "Suggerimenti per la Ricerca",
|
||||
"updated_at": "Aggiornato Il"
|
||||
},
|
||||
"profile": {
|
||||
"active": "Attivo",
|
||||
"change_password": "Cambia Password",
|
||||
"currency_format": "Formato Valuta",
|
||||
"current_password": "Password Corrente",
|
||||
"delete_account": "Elimina Account",
|
||||
"delete_account_sub": "Elimina il tuo account e tutti i dati associati. Questa operazione non può essere annullata.",
|
||||
"enabled": "Abilitato",
|
||||
"gen_invite": "Genera Link di Invito",
|
||||
"group_settings": "Impostazioni Gruppo",
|
||||
"group_settings_sub": "Impostazioni Gruppo Condivise. Potrebbe essere necessario aggiornare il browser affinché alcune impostazioni vengano applicate.",
|
||||
"inactive": "Inattivo",
|
||||
"new_password": "Nuova Password",
|
||||
"notifier_modal": "{ type, select, true {Modifica} false {Crea} other {Altro}} Notifier",
|
||||
"notifiers": "Notifiche",
|
||||
"notifiers_sub": "Ricevi notifiche per i prossimi promemoria di manutenzione",
|
||||
"test": "Test",
|
||||
"theme_settings": "Impostazioni Tema",
|
||||
"theme_settings_sub": "Le impostazioni del tema sono memorizzate nella memoria locale del tuo browser. Puoi cambiare il tema \nin qualsiasi momento. Se hai problemi a impostare il tuo tema, prova a ricaricare la pagina.",
|
||||
"update_group": "Aggiorna Gruppo",
|
||||
"url": "URL",
|
||||
"user_profile": "Profilo Utente",
|
||||
"user_profile_sub": "Invita utenti e gestisci il tuo account."
|
||||
}
|
||||
}
|
||||
128
frontend/locales/nl.json
Normal file
128
frontend/locales/nl.json
Normal file
@@ -0,0 +1,128 @@
|
||||
{
|
||||
"global": {
|
||||
"version": "Versie: { version }",
|
||||
"github": "GitHub Project",
|
||||
"join_discord": "Sluit je aan bij de Discord",
|
||||
"follow_dev": "Volg de ontwikkelaar",
|
||||
"read_docs": "Lees de documentatie",
|
||||
"email": "E-mail",
|
||||
"submit": "Indienen",
|
||||
"confirm": "Bevestigen",
|
||||
"create": "Maken",
|
||||
"create_and_add": "Maak en voeg nog een toe",
|
||||
"password": "Wachtwoord",
|
||||
"build": "Bouw: { build }",
|
||||
"created": "Gemaakt",
|
||||
"welcome": "Welkom, { username }",
|
||||
"sign_out": "Log uit",
|
||||
"labels": "etiketten",
|
||||
"locations": "Locaties",
|
||||
"name": "Naam",
|
||||
"items": "artikelen",
|
||||
"search": "Zoeken"
|
||||
},
|
||||
"index": {
|
||||
"disabled_registration": "Registratie uitgeschakeld",
|
||||
"login": "Log in",
|
||||
"register": "Registreer",
|
||||
"remember_me": "Onthoud mij",
|
||||
"set_email": "Wat is je mailadres?",
|
||||
"set_password": "Stel je wachtwoord in",
|
||||
"set_name": "Wat is je naam?",
|
||||
"joining_group": "Je neemt deel aan een bestaande groep!",
|
||||
"tagline": "Volg, Organiseer en beheer je dingen.",
|
||||
"dont_join_group": "Wil je niet aan een groep deelnemen?"
|
||||
},
|
||||
"components": {
|
||||
"global": {
|
||||
"password_score": {
|
||||
"password_strength": "Wachtwoord sterkte"
|
||||
},
|
||||
"page_qr_code": {
|
||||
"page_url": "Pagina URL"
|
||||
}
|
||||
},
|
||||
"app": {
|
||||
"import_dialog": {
|
||||
"title": "Importeer CSV bestand",
|
||||
"change_warning": "Gedrag voor importeren met bestaande import_refs is veranderd. Als een import_refs reeds bestaat in het CSV bestand, het\nobject zal worden aangepast met de waardes van het CSV bestand.",
|
||||
"upload": "Upload",
|
||||
"description": "Importeer een CSV bestand met je objecten, labels en locaties. Zie documentatie voor meer informatie m.b.t.\n vereist format."
|
||||
}
|
||||
},
|
||||
"item": {
|
||||
"create_modal": {
|
||||
"title": "Maak object",
|
||||
"photo_button": "Foto 📷"
|
||||
},
|
||||
"view": {
|
||||
"selectable": {
|
||||
"items": "Objecten",
|
||||
"card": "Kaart",
|
||||
"table": "Tabel",
|
||||
"no_items": "Geen objecten om te tonen"
|
||||
}
|
||||
}
|
||||
},
|
||||
"label": {
|
||||
"create_modal": {
|
||||
"title": "Maak label"
|
||||
}
|
||||
},
|
||||
"location": {
|
||||
"create_modal": {
|
||||
"title": "Maak locatie"
|
||||
}
|
||||
}
|
||||
},
|
||||
"profile": {
|
||||
"gen_invite": "Genereer Uitnodigingslink",
|
||||
"notifier_modal": "{ type, select, true {Bewerk} false {Creëer} other {overig}} Notifier",
|
||||
"change_password": "Verander Wachtwoord",
|
||||
"current_password": "Huidig Wachtwoord",
|
||||
"new_password": "Nieuw Wachtwoord",
|
||||
"url": "URL",
|
||||
"enabled": "ingeschakeld",
|
||||
"test": "Test",
|
||||
"user_profile": "Gebruikers Profiel",
|
||||
"user_profile_sub": "Nodig gebruikers uit, en beheer je account.",
|
||||
"active": "Actief",
|
||||
"inactive": "Inactief",
|
||||
"notifiers_sub": "Krijg notificaties voor opkomende onderhouds herinneringen",
|
||||
"group_settings_sub": "Gedeelde groepsinstellingen",
|
||||
"group_settings": "Groeps Instellingen",
|
||||
"notifiers": "Notificatie",
|
||||
"currency_format": "Valutanotatie",
|
||||
"update_group": "Groep bijwerken",
|
||||
"theme_settings": "Theme instellingen",
|
||||
"theme_settings_sub": "Thema-instellingen worden opgeslagen in de lokale opslag van uw browser. Je kan deze wijzigen op elk moment. \nAls je problemen hebt met de instellingen kun je je browser verversen.",
|
||||
"delete_account": "Verwijder account",
|
||||
"delete_account_sub": "Verwijder je account en alle geassocieerde data. Deze actie kan niet ongedaan worden."
|
||||
},
|
||||
"items": {
|
||||
"tip_1": "Locatie- en labelfilters gebruiken de 'OF' -werking. Als er meer dan een is geselecteerd,\nis er maar een nodig voor een overeenkomst.",
|
||||
"tip_2": "Zoekopdrachten voorafgegaan door '#'' zullen om een object-ID vragen (bijvoorbeeld '#000-001')",
|
||||
"tip_3": "Veldfilters gebruiken de 'OF' -bewerking. Indien meer dan 1 is geselecteerd\nzal er maar 1 nodig zijn voor een match.",
|
||||
"tips_sub": "Zoektips",
|
||||
"updated_at": "Bijgewerkt op",
|
||||
"pages": "Pagina { page } van { totalPages }",
|
||||
"prev_page": "Vorige pagina",
|
||||
"query_id": "ID-nummer van object opvragen: { id }",
|
||||
"reset_search": "Reset Zoeken",
|
||||
"tips": "Tips",
|
||||
"no_results": "Geen Items Gevonden",
|
||||
"options": "Opties",
|
||||
"order_by": "Sorteren op",
|
||||
"results": "{ total } Resultaten",
|
||||
"add": "Toevoegen",
|
||||
"created_at": "Aangemaakt op",
|
||||
"custom_fields": "Aangepaste velden",
|
||||
"field_selector": "Veld selectie",
|
||||
"field_value": "Veldwaarde",
|
||||
"first": "Eerst",
|
||||
"include_archive": "Inclusief gearchiveerde items",
|
||||
"last": "Achternaam",
|
||||
"negate_labels": "Negeer Geselecteerde Etiketten",
|
||||
"next_page": "Volgende pagina"
|
||||
}
|
||||
}
|
||||
118
frontend/locales/pl.json
Normal file
118
frontend/locales/pl.json
Normal file
@@ -0,0 +1,118 @@
|
||||
{
|
||||
"components": {
|
||||
"app": {
|
||||
"import_dialog": {
|
||||
"description": "Zaimportuj plik CSV zawierający Twoje przedmioty, etykiety i lokalizacje. Zobacz dokumentację, aby uzyskać \nwięcej informacji na temat wymaganego formatu.",
|
||||
"title": "Zaimportuj plik CSV",
|
||||
"upload": "Prześlij"
|
||||
}
|
||||
},
|
||||
"global": {
|
||||
"page_qr_code": {
|
||||
"page_url": "Adres URL strony"
|
||||
},
|
||||
"password_score": {
|
||||
"password_strength": "Siła hasła"
|
||||
}
|
||||
},
|
||||
"item": {
|
||||
"create_modal": {
|
||||
"title": "Utwórz przedmiot",
|
||||
"photo_button": "Zdjęcie 📷"
|
||||
},
|
||||
"view": {
|
||||
"selectable": {
|
||||
"card": "Karta",
|
||||
"items": "Przedmioty",
|
||||
"no_items": "Brak przedmiotów do wyświetlenia",
|
||||
"table": "Tabela"
|
||||
}
|
||||
}
|
||||
},
|
||||
"label": {
|
||||
"create_modal": {
|
||||
"title": "Stwórz nową etykietę"
|
||||
}
|
||||
},
|
||||
"location": {
|
||||
"create_modal": {
|
||||
"title": "Utwórz lokalizację"
|
||||
}
|
||||
}
|
||||
},
|
||||
"global": {
|
||||
"build": "Kompilacja: {build}",
|
||||
"follow_dev": "Śledź dewelopera",
|
||||
"github": "Projekt na GitHubie",
|
||||
"items": "Przedmioty",
|
||||
"version": "Wersja:{version}",
|
||||
"welcome": "Witaj, {username}",
|
||||
"confirm": "Potwierdź",
|
||||
"create": "Utwórz",
|
||||
"create_and_add": "Utwórz i dodaj kolejny",
|
||||
"created": "Utworzone",
|
||||
"email": "E-mail",
|
||||
"join_discord": "Dołącz do Discorda",
|
||||
"labels": "Etykiety",
|
||||
"locations": "Lokalizacje",
|
||||
"name": "Nazwa",
|
||||
"password": "Hasło",
|
||||
"read_docs": "Przeczytaj dokumentację",
|
||||
"search": "Wyszukaj",
|
||||
"sign_out": "Wyloguj się",
|
||||
"submit": "Wyślij"
|
||||
},
|
||||
"index": {
|
||||
"set_password": "Ustaw swoje hasło",
|
||||
"dont_join_group": "Nie chcesz dołączyć do grupy?",
|
||||
"disabled_registration": "Rejestracja jest wyłączona",
|
||||
"joining_group": "Dołączasz do istniejącej grupy!",
|
||||
"login": "Zaloguj się",
|
||||
"register": "Zarejestruj się",
|
||||
"remember_me": "Zapamiętaj mnie",
|
||||
"set_email": "Jaki jest Twój adres e-mail?",
|
||||
"set_name": "Jak się nazywasz?",
|
||||
"tagline": "Śledź, organizuj i zarządzaj swoimi rzeczami."
|
||||
},
|
||||
"items": {
|
||||
"created_at": "Data utworzenia",
|
||||
"field_selector": "Selektor pól",
|
||||
"field_value": "Wartość pola",
|
||||
"first": "Pierwszy",
|
||||
"include_archive": "Uwzględnij zarchiwizowane przedmioty",
|
||||
"negate_labels": "Neguj wybrane etykiety",
|
||||
"no_results": "Nie znaleziono przedmiotów",
|
||||
"query_id": "Zapytanie o numer identyfikacyjny zasobu: { id }",
|
||||
"results": "{ total } wyniki",
|
||||
"tip_3": "Filtry pól używają operacji 'LUB'. Jeśli wybrano więcej niż jeden, wystarczy jeden, \naby uzyskać dopasowanie.",
|
||||
"updated_at": "Zaktualizowano",
|
||||
"tip_1": "Filtry lokalizacji i etykiet używają operacji 'LUB'. Jeśli wybrano więcej niż jeden, wystarczy jeden, \naby uzyskać dopasowanie.",
|
||||
"pages": "Strona {page} z {totalPages}",
|
||||
"add": "Dodaj",
|
||||
"custom_fields": "Pola niestandardowe",
|
||||
"last": "Ostatni",
|
||||
"next_page": "Następna strona",
|
||||
"options": "Opcje",
|
||||
"prev_page": "Poprzednia strona",
|
||||
"reset_search": "Zresetuj wyszukiwanie",
|
||||
"tip_2": "Wyszukiwania poprzedzone prefiksem \"#\" będą wysyłać zapytanie o identyfikator zasobu (na przykład \"#000-001\")",
|
||||
"tips": "Wskazówki",
|
||||
"tips_sub": "Wskazówki wyszukiwania",
|
||||
"order_by": "Ułóż według"
|
||||
},
|
||||
"profile": {
|
||||
"active": "Aktywny",
|
||||
"change_password": "Zmiana hasła",
|
||||
"currency_format": "Format waluty",
|
||||
"delete_account": "Usuń konto",
|
||||
"delete_account_sub": "Usuń swoje konto oraz wszystkie powiązane z nim dane. Tego nie można cofnąć.",
|
||||
"current_password": "Bieżące hasło",
|
||||
"group_settings_sub": "Ustawienia grupy udostępnione. Możesz potrzebować odświeżyć przeglądarkę, aby niektóre ustawienia zostały zastosowane.",
|
||||
"inactive": "Nieaktywny",
|
||||
"new_password": "Nowe hasło",
|
||||
"notifier_modal": "{ type, select, true {Edit} false {Create} other {Other}} Powiadomiony",
|
||||
"enabled": "Włączone",
|
||||
"gen_invite": "Wygeneruj link z zaproszeniem",
|
||||
"group_settings": "Ustawienia grupy"
|
||||
}
|
||||
}
|
||||
128
frontend/locales/ru.json
Normal file
128
frontend/locales/ru.json
Normal file
@@ -0,0 +1,128 @@
|
||||
{
|
||||
"components": {
|
||||
"app": {
|
||||
"import_dialog": {
|
||||
"title": "Импорт CSV файла",
|
||||
"change_warning": "Изменено поведение для импортов с существующими import_ref. Если в CSV-файле присутствует import_ref, \nто элемент будет обновлен значениями из CSV-файла.",
|
||||
"description": "Импортируйте CSV-файл, содержащий ваши предметы, ярлыки и местоположения. Для получения информации о требуемом формате \nсм. документацию.",
|
||||
"upload": "Загрузить"
|
||||
}
|
||||
},
|
||||
"global": {
|
||||
"page_qr_code": {
|
||||
"page_url": "URL-адрес страницы"
|
||||
},
|
||||
"password_score": {
|
||||
"password_strength": "Сложность пароля"
|
||||
}
|
||||
},
|
||||
"item": {
|
||||
"create_modal": {
|
||||
"title": "Создать элемент",
|
||||
"photo_button": "Фото 📷"
|
||||
},
|
||||
"view": {
|
||||
"selectable": {
|
||||
"items": "Элементы",
|
||||
"no_items": "Нет элементов для отображения",
|
||||
"table": "Таблица",
|
||||
"card": "Карта"
|
||||
}
|
||||
}
|
||||
},
|
||||
"location": {
|
||||
"create_modal": {
|
||||
"title": "Создать локацию"
|
||||
}
|
||||
},
|
||||
"label": {
|
||||
"create_modal": {
|
||||
"title": "Создать ярлык"
|
||||
}
|
||||
}
|
||||
},
|
||||
"global": {
|
||||
"email": "Email",
|
||||
"items": "Элементы",
|
||||
"build": "Сборка: { build }",
|
||||
"confirm": "Подтвердить",
|
||||
"create": "Создать",
|
||||
"create_and_add": "Создать и добавить еще",
|
||||
"created": "Создано",
|
||||
"follow_dev": "Следить за разработчиком",
|
||||
"github": "Проект Github",
|
||||
"labels": "Ярлыки",
|
||||
"join_discord": "Присоединяйтесь к Discord",
|
||||
"locations": "Локации",
|
||||
"name": "Имя",
|
||||
"password": "Пароль",
|
||||
"read_docs": "Прочитать документацию",
|
||||
"search": "Поиск",
|
||||
"sign_out": "Выйти",
|
||||
"submit": "Отправить",
|
||||
"version": "Версия: { version }",
|
||||
"welcome": "Добро пожаловать, { username }"
|
||||
},
|
||||
"index": {
|
||||
"login": "Войти",
|
||||
"register": "Зарегистрироваться",
|
||||
"set_email": "Какой у вас адрес электронной почты?",
|
||||
"set_name": "Как вас зовут?",
|
||||
"set_password": "Установите пароль",
|
||||
"tagline": "Отслеживайте, упорядочивайте и управляйте своими вещами.",
|
||||
"disabled_registration": "Регистрация отключена",
|
||||
"dont_join_group": "Не хотите ли вступить в группу?",
|
||||
"remember_me": "Запомнить меня",
|
||||
"joining_group": "Вы присоединяетесь к уже существующей группе!"
|
||||
},
|
||||
"items": {
|
||||
"add": "Добавить",
|
||||
"created_at": "Создано в",
|
||||
"custom_fields": "Настраиваемые поля",
|
||||
"first": "Первый",
|
||||
"pages": "Страница {page} из {totalPages}",
|
||||
"prev_page": "Предыдущая страница",
|
||||
"reset_search": "Сбросить Поиск",
|
||||
"field_selector": "Поле выбора",
|
||||
"field_value": "Значение поля",
|
||||
"last": "Последний",
|
||||
"negate_labels": "Снять выбранные ярлыки",
|
||||
"next_page": "Следующая страница",
|
||||
"no_results": "Элементы не найдены",
|
||||
"options": "Параметры",
|
||||
"results": "{ total } Результатов",
|
||||
"tips": "Подсказки",
|
||||
"tips_sub": "Поисковые подсказки",
|
||||
"updated_at": "Обновлено в",
|
||||
"tip_3": "Фильтры по полю используют операцию «ИЛИ». Если выбрано несколько фильтров, для совпадения\n требуется только один.",
|
||||
"tip_2": "Поисковые запросы с префиксом \"#\" должны включать в себя ID актива (прим. '#000-001')",
|
||||
"include_archive": "Включить архивированные элементы",
|
||||
"order_by": "Сортировка по",
|
||||
"query_id": "Запрос идентификационного номера актива: { id }",
|
||||
"tip_1": "При фильтрации по локации и по ярлыкам используется логический оператор «ИЛИ». Если выбрано несколько фильтров, то для срабатывания\n требуется лишь одно совпадение."
|
||||
},
|
||||
"profile": {
|
||||
"new_password": "Новый пароль",
|
||||
"update_group": "Обновить группу",
|
||||
"active": "Активный",
|
||||
"change_password": "Изменить пароль",
|
||||
"currency_format": "Формат валюты",
|
||||
"current_password": "Текущий пароль",
|
||||
"delete_account": "Удалить аккаунт",
|
||||
"group_settings": "Настройки группы",
|
||||
"inactive": "Неактивный",
|
||||
"notifier_modal": "{ type, select, true {Edit} false {Create} other {Other}} Уведомитель",
|
||||
"notifiers": "Уведомители",
|
||||
"notifiers_sub": "Получить уведомление о предстоящем обслуживании",
|
||||
"test": "Тест",
|
||||
"theme_settings": "Настройки темы",
|
||||
"url": "URL",
|
||||
"user_profile": "Профиль пользователя",
|
||||
"user_profile_sub": "Приглашайте пользователей и управляйте своим аккаунтом.",
|
||||
"enabled": "Активен",
|
||||
"theme_settings_sub": "Настройки темы хранятся в локальном хранилище браузера. Вы можете изменить тему в любое время. Если у вас\n не удается установить тему, попробуйте перезапустить браузер.",
|
||||
"delete_account_sub": "Удалить свой аккаунт и все связанные с ним данные. Это действие невозможно отменить.",
|
||||
"gen_invite": "Сгенерировать ссылку-приглашение",
|
||||
"group_settings_sub": "Настройки общей группы. Для применения изменений возможно потребуется перезагрузить страницу."
|
||||
}
|
||||
}
|
||||
128
frontend/locales/sl.json
Normal file
128
frontend/locales/sl.json
Normal file
@@ -0,0 +1,128 @@
|
||||
{
|
||||
"global": {
|
||||
"follow_dev": "Sledi razvijjalcu",
|
||||
"build": "Gradnja: [ build ]",
|
||||
"confirm": "Potrdi",
|
||||
"create": "Ustvari",
|
||||
"create_and_add": "Ustvari in dodaj še enega",
|
||||
"created": "Ustvarjeno",
|
||||
"email": "E-pošta",
|
||||
"github": "GitHub projekt",
|
||||
"items": "Predmeti",
|
||||
"join_discord": "Pridruži se na Discord",
|
||||
"labels": "Oznake",
|
||||
"locations": "Lokacije",
|
||||
"name": "Naziv",
|
||||
"password": "Geslo",
|
||||
"read_docs": "Preberite dokumentacijo",
|
||||
"search": "Iskanje",
|
||||
"sign_out": "Odjava",
|
||||
"submit": "Pošlji",
|
||||
"version": "Verzija: { version }",
|
||||
"welcome": "Dobrodošel, { username }"
|
||||
},
|
||||
"components": {
|
||||
"app": {
|
||||
"import_dialog": {
|
||||
"change_warning": "Vedenje pri uvozih z obstoječimi import_refs se je spremenilo. Če je import_ref prisoten v datoteki CSV, bo\nelement posodobljen z vrednostmi v datoteki CSV.",
|
||||
"description": "Uvozite datoteko CSV, ki vsebuje vaše predmete, oznake in lokacije. Poglejte si dokumentacijo za več informacij o\nzahtevani obliki.",
|
||||
"title": "Uvozi CSV datoteko",
|
||||
"upload": "Naloži"
|
||||
}
|
||||
},
|
||||
"global": {
|
||||
"page_qr_code": {
|
||||
"page_url": "URL strani"
|
||||
},
|
||||
"password_score": {
|
||||
"password_strength": "Moč gesla"
|
||||
}
|
||||
},
|
||||
"item": {
|
||||
"create_modal": {
|
||||
"title": "Ustvari predmet",
|
||||
"photo_button": "Fotografija 📷"
|
||||
},
|
||||
"view": {
|
||||
"selectable": {
|
||||
"card": "Kartica",
|
||||
"items": "Predmeti",
|
||||
"no_items": "Ni predmetov za prikaz",
|
||||
"table": "Tabela"
|
||||
}
|
||||
}
|
||||
},
|
||||
"label": {
|
||||
"create_modal": {
|
||||
"title": "Ustvari oznako"
|
||||
}
|
||||
},
|
||||
"location": {
|
||||
"create_modal": {
|
||||
"title": "Ustvari lokacijo"
|
||||
}
|
||||
}
|
||||
},
|
||||
"index": {
|
||||
"disabled_registration": "Registracija je onemogočena",
|
||||
"dont_join_group": "Se ne želite pridružiti skupini?",
|
||||
"joining_group": "Pridružujete se obstoječi skupini!",
|
||||
"login": "Prijava",
|
||||
"register": "Registriraj se",
|
||||
"remember_me": "Zapomni si me",
|
||||
"set_email": "Kakšen je vaš e-poštni naslov?",
|
||||
"set_password": "Nastavite geslo",
|
||||
"tagline": "Sledite, organizirajte in upravljajte svoje stvari.",
|
||||
"set_name": "Kako vam je ime?"
|
||||
},
|
||||
"items": {
|
||||
"created_at": "Ustvarjeno ob",
|
||||
"include_archive": "Vključi arhivirane predmete",
|
||||
"last": "Zadnji",
|
||||
"negate_labels": "Negiraj izbrane oznake",
|
||||
"no_results": "Ni najdenih predmetov",
|
||||
"options": "Možnosti",
|
||||
"pages": "Stran { page } od { totalPages }",
|
||||
"tip_2": "Iskanja s predpono '#' bodo iskala ID sredstva (primer '#000-001')",
|
||||
"tip_3": "Filtri polj uporabljajo operacijo 'ALI'. Če je izbranih več kot eno, bo za ujemanje dovolj samo\n eno.",
|
||||
"tips": "Nasveti",
|
||||
"updated_at": "Posodobljeno ob",
|
||||
"custom_fields": "Polje po meri",
|
||||
"results": "Rezultatov: { total }",
|
||||
"tip_1": "Filtri lokacij in oznak uporabljajo operacijo 'ALI'. Če je izbranih več kot ena, bo izbrana samo ena\n potrebna za ujemanje.",
|
||||
"add": "Dodaj",
|
||||
"field_selector": "Izbirnik polj",
|
||||
"field_value": "Vrednost polja",
|
||||
"first": "Prvi",
|
||||
"next_page": "Naslednja stran",
|
||||
"prev_page": "Prejšnja stran",
|
||||
"query_id": "Poizvedovanje po identifikacijski številki sredstva: { id }",
|
||||
"reset_search": "Ponastavi iskanje",
|
||||
"tips_sub": "Nasveti za iskanje",
|
||||
"order_by": "Razvrsti po"
|
||||
},
|
||||
"profile": {
|
||||
"gen_invite": "Ustvari povezavo povabila",
|
||||
"group_settings_sub": "Nastavitve skupine v skupni rabi. Morda boste morali osvežiti brskalnik, da bodo nekatere nastavitve veljale.",
|
||||
"notifiers": "Obveščevalci",
|
||||
"notifier_modal": "{ type, select, true {Edit} false {Create} other {Other}} Obveščevalec",
|
||||
"notifiers_sub": "Prejemajte obvestila za prihajajoče opomnike o vzdrževanju",
|
||||
"theme_settings_sub": "Nastavitve teme so shranjene v lokalni shrambi vašega brskalnika. Temo lahko kadar koli spremenite. Če\n imate težave z nastavitvijo teme, poskusite osvežiti brskalnik.",
|
||||
"update_group": "Posodobi skupino",
|
||||
"url": "URL",
|
||||
"delete_account_sub": "Izbrišite svoj račun in vse z njim povezane podatke. Tega ni mogoče razveljaviti.",
|
||||
"active": "Aktivno",
|
||||
"change_password": "Sprememba gesla",
|
||||
"currency_format": "Oblika valute",
|
||||
"current_password": "Trenutno geslo",
|
||||
"delete_account": "Izbriši račun",
|
||||
"enabled": "Omogočeno",
|
||||
"group_settings": "Nastavitve skupine",
|
||||
"inactive": "Neaktivno",
|
||||
"new_password": "Novo geslo",
|
||||
"test": "Preizkus",
|
||||
"theme_settings": "Nastavitve teme",
|
||||
"user_profile": "Profil uporabnika",
|
||||
"user_profile_sub": "Povabite uporabnike in upravljajte svoj račun."
|
||||
}
|
||||
}
|
||||
128
frontend/locales/sv.json
Normal file
128
frontend/locales/sv.json
Normal file
@@ -0,0 +1,128 @@
|
||||
{
|
||||
"profile": {
|
||||
"notifier_modal": "{ type, select, true {Edit} false {Create} other {Other}} Anmälare",
|
||||
"change_password": "Ändra Lösenord",
|
||||
"current_password": "Nuvarande lösenord",
|
||||
"new_password": "Nytt lösenord",
|
||||
"notifiers_sub": "Få aviseringar om kommande underhållspåminnelser",
|
||||
"url": "URL",
|
||||
"test": "Test",
|
||||
"gen_invite": "Skapa inbjudningslänk",
|
||||
"user_profile": "Användarprofil",
|
||||
"user_profile_sub": "Bjud in användare och hantera ditt konto.",
|
||||
"active": "Aktiv",
|
||||
"inactive": "Inaktiv",
|
||||
"notifiers": "Notiser",
|
||||
"enabled": "Aktiverad",
|
||||
"currency_format": "Valuta format",
|
||||
"delete_account": "Radera konto",
|
||||
"delete_account_sub": "Ta bort ditt konto och alla tillhörande data. Detta kan inte ångras.",
|
||||
"group_settings": "Grupp inställningar",
|
||||
"group_settings_sub": "Inställningar för delad grupp. Du kan behöva uppdatera din webbläsare för att vissa inställningar ska gälla.",
|
||||
"theme_settings": "Temainställningar",
|
||||
"theme_settings_sub": "Temainställningar sparas i din webbläsares lokala lagring. Du kan ändra tema när du vill. Om du\nhar problem att ställa in tema, pröva att ladda om din webbläsare.",
|
||||
"update_group": "Uppdatera grupp"
|
||||
},
|
||||
"index": {
|
||||
"set_name": "Vad heter du?",
|
||||
"joining_group": "Du går med i en befintlig grupp!",
|
||||
"dont_join_group": "Vill du inte gå med i en grupp?",
|
||||
"tagline": "Spåra, organisera och hantera dina saker.",
|
||||
"disabled_registration": "Registrering avaktiverad",
|
||||
"login": "Logga in",
|
||||
"register": "Registrera",
|
||||
"remember_me": "Kom ihåg mig",
|
||||
"set_email": "Vad är din e-post?",
|
||||
"set_password": "Ställ in ditt lösenord"
|
||||
},
|
||||
"components": {
|
||||
"app": {
|
||||
"import_dialog": {
|
||||
"upload": "Ladda upp",
|
||||
"title": "Importera CSV fil",
|
||||
"description": "Importera en CSV-fil som innehåller dina föremål, etiketter och platser. Se dokumentationen för mer information om \nönskat format.",
|
||||
"change_warning": "Beteendet för importer med befintliga import_refs har ändrats. Om en import_ref finns i CSV-filen, \nobjektet kommer att uppdateras med värdena i CSV-filen."
|
||||
}
|
||||
},
|
||||
"item": {
|
||||
"view": {
|
||||
"selectable": {
|
||||
"card": "Kort",
|
||||
"items": "Föremål",
|
||||
"table": "Tabell",
|
||||
"no_items": "Inga föremål att visa"
|
||||
}
|
||||
},
|
||||
"create_modal": {
|
||||
"title": "Skapa föremål",
|
||||
"photo_button": "Foto 📷"
|
||||
}
|
||||
},
|
||||
"label": {
|
||||
"create_modal": {
|
||||
"title": "Skapa etikett"
|
||||
}
|
||||
},
|
||||
"location": {
|
||||
"create_modal": {
|
||||
"title": "Skapa plats"
|
||||
}
|
||||
},
|
||||
"global": {
|
||||
"password_score": {
|
||||
"password_strength": "Lösenordsstyrka"
|
||||
},
|
||||
"page_qr_code": {
|
||||
"page_url": "Sidans URL"
|
||||
}
|
||||
}
|
||||
},
|
||||
"global": {
|
||||
"build": "Byggd: { build }",
|
||||
"github": "GitHub Projekt",
|
||||
"join_discord": "Gå med i Discord",
|
||||
"follow_dev": "Följ utvecklaren",
|
||||
"read_docs": "Läs dokumenten",
|
||||
"password": "Lösenord",
|
||||
"email": "Epost",
|
||||
"submit": "Skicka",
|
||||
"confirm": "Godkänn",
|
||||
"create": "Skapa",
|
||||
"created": "Skapad",
|
||||
"welcome": "Välkommen, { username }",
|
||||
"sign_out": "Logga ut",
|
||||
"create_and_add": "Skapa och lägg till en annan",
|
||||
"version": "Version: { version }",
|
||||
"items": "Föremål",
|
||||
"labels": "Etiketter",
|
||||
"locations": "Platser",
|
||||
"name": "Namn",
|
||||
"search": "Sök"
|
||||
},
|
||||
"items": {
|
||||
"add": "Lägg till",
|
||||
"created_at": "Skapat",
|
||||
"custom_fields": "Egna fält",
|
||||
"field_selector": "Fält alternativ",
|
||||
"field_value": "Fält värde",
|
||||
"first": "Första",
|
||||
"include_archive": "Inkludera arkiverade föremål",
|
||||
"last": "Sista",
|
||||
"negate_labels": "Negera valda etiketter",
|
||||
"next_page": "Nästa sida",
|
||||
"no_results": "Inga föremål hittades",
|
||||
"options": "Alternativ",
|
||||
"order_by": "Ordning via",
|
||||
"pages": "Sida { page } av { totalPages }",
|
||||
"prev_page": "Föregående sida",
|
||||
"query_id": "Fråga efter tillgångs-ID-nummer: { id }",
|
||||
"reset_search": "Återställ sökning",
|
||||
"tip_1": "Platser och etiketter filter använder 'OR' funktionen. Om fler än en är valda, endast en kommer\nkrävas för en träff.",
|
||||
"tip_2": "Sökningar med prefixet '#'' kommer att fråga efter ett tillgångs-ID (exempel '#000-001')",
|
||||
"tip_3": "Fältfilter använder 'OR' funktion. Om fler än en är valda endast en kommer att bli krävande för en\nträff.",
|
||||
"tips": "Tips",
|
||||
"tips_sub": "Sök Tips",
|
||||
"updated_at": "Uppdaterad",
|
||||
"results": "{ total } Resultat"
|
||||
}
|
||||
}
|
||||
128
frontend/locales/tr.json
Normal file
128
frontend/locales/tr.json
Normal file
@@ -0,0 +1,128 @@
|
||||
{
|
||||
"global": {
|
||||
"version": "Versiyon:{ version }",
|
||||
"password": "Şifre",
|
||||
"create": "Oluştur",
|
||||
"github": "GitHub projesi",
|
||||
"join_discord": "Discord'a Katılın",
|
||||
"follow_dev": "Geliştiriciyi takip edin",
|
||||
"read_docs": "Dokümanları okuyun",
|
||||
"email": "Elektronik posta",
|
||||
"submit": "Gönder",
|
||||
"confirm": "Onaylayın",
|
||||
"build": "Sürüm: { build }",
|
||||
"create_and_add": "Oluştur ve Bir Tane Daha Ekle",
|
||||
"created": "Oluşturuldu",
|
||||
"items": "Öğeler",
|
||||
"labels": "Etiketler",
|
||||
"locations": "Konumlar",
|
||||
"name": "İsim",
|
||||
"search": "Ara",
|
||||
"sign_out": "Oturumu kapat",
|
||||
"welcome": "Hoşgeldib, { username }"
|
||||
},
|
||||
"components": {
|
||||
"app": {
|
||||
"import_dialog": {
|
||||
"change_warning": "Mevcut import_refs ile içe aktarmaların davranışı değişti. CSV dosyasında bir import_ref varsa, \nöğe CSV dosyasındaki değerlerle güncellenecektir.",
|
||||
"title": "CSV dosyasını içeri aktar",
|
||||
"upload": "Yükle",
|
||||
"description": "Öğelerinizi, etiketlerinizi ve konumlarınızı içeren bir CSV dosyasını içe aktarın. Daha fazla\nbilgi için dökümanları okuyun."
|
||||
}
|
||||
},
|
||||
"global": {
|
||||
"password_score": {
|
||||
"password_strength": "Şifre güvenlik seviyesi"
|
||||
},
|
||||
"page_qr_code": {
|
||||
"page_url": "Sayfa URL'si"
|
||||
}
|
||||
},
|
||||
"item": {
|
||||
"create_modal": {
|
||||
"title": "Eşya Oluştur",
|
||||
"photo_button": "Fotoğraf 📷"
|
||||
},
|
||||
"view": {
|
||||
"selectable": {
|
||||
"items": "Öğeler",
|
||||
"card": "Kart",
|
||||
"table": "Tablo",
|
||||
"no_items": "Görüntülecek Öge Yok"
|
||||
}
|
||||
}
|
||||
},
|
||||
"label": {
|
||||
"create_modal": {
|
||||
"title": "Etiket oluştur"
|
||||
}
|
||||
},
|
||||
"location": {
|
||||
"create_modal": {
|
||||
"title": "Konum oluştur"
|
||||
}
|
||||
}
|
||||
},
|
||||
"index": {
|
||||
"remember_me": "Beni Hatırla",
|
||||
"tagline": "Eşyalarınızı Takip Edin, Düzenleyin ve Yönetin.",
|
||||
"disabled_registration": "Kayıt olma devre dışı",
|
||||
"login": "Oturum Aç",
|
||||
"register": "Kaydolun",
|
||||
"set_email": "E-posta adresiniz nedir?",
|
||||
"set_password": "Şifrenizi belirleyin",
|
||||
"set_name": "Adın ne?",
|
||||
"joining_group": "Mevcut bir gruba katılıyorsunuz!",
|
||||
"dont_join_group": "Bir gruba katılmak istemiyor musunuz?"
|
||||
},
|
||||
"items": {
|
||||
"add": "Ekle",
|
||||
"created_at": "Oluşturulma",
|
||||
"custom_fields": "Özel Alanlar",
|
||||
"field_selector": "alan seçici",
|
||||
"field_value": "Alan Değeri",
|
||||
"first": "Birinci",
|
||||
"include_archive": "Arşivlenen Öğeleri Dahil Et",
|
||||
"last": "Son",
|
||||
"negate_labels": "Seçili Etiketleri Yoksay",
|
||||
"next_page": "Sonraki Sayfa",
|
||||
"no_results": "Öğe Bulunamadı",
|
||||
"options": "Seçenekler",
|
||||
"order_by": "Sıralama ölçütü",
|
||||
"pages": "Sayfa { page }/{ totalPages }",
|
||||
"prev_page": "Önceki Sayfa",
|
||||
"query_id": "Varlık Kimlik Numarası Sorgulanıyor: { id }",
|
||||
"tip_1": "Konum ve etiket filtreleri 'veya' işlemini kullanır. Eğer birden fazla seçilirse sadece biri \neşleştirme için kullanılacaktır.",
|
||||
"reset_search": "Aramayı Sıfırla",
|
||||
"tips": "İpuçları",
|
||||
"results": "{ total } Sonuç",
|
||||
"tip_2": "'#' ile başlayan aramalar bir varlık kimliğini sorgular (örneğin '#000-001')",
|
||||
"tip_3": "Alan filtreleri 'VEYA' işlemini kullanır. Birden fazla seçenek seçilirse, eşleşme için yalnızca birinin \nkarşılanması yeterlidir.",
|
||||
"tips_sub": "Arama İpuçları",
|
||||
"updated_at": "Güncellendiği Zaman"
|
||||
},
|
||||
"profile": {
|
||||
"url": "URL",
|
||||
"user_profile": "Kullanıcı Profili",
|
||||
"user_profile_sub": "Kullanıcıları davet edin ve hesabınızı yönetin.",
|
||||
"active": "Aktif",
|
||||
"change_password": "Şifre Değiştir",
|
||||
"currency_format": "Para Birimi Biçimi",
|
||||
"current_password": "Mevcut Şifre",
|
||||
"delete_account": "Hesabı Sil",
|
||||
"delete_account_sub": "Hesabınızı ve ona bağlı tüm verileri silin. Bu işlem geri alınamaz.",
|
||||
"enabled": "Etkinleştirildi",
|
||||
"gen_invite": "Davet Bağlantısı Oluştur",
|
||||
"group_settings": "Grup Ayarları",
|
||||
"group_settings_sub": "Paylaşılan Grup Ayarları. Bazı ayarların uygulanabilmesi için tarayıcınızı yenilemeniz gerekebilir.",
|
||||
"inactive": "Etkin Değil",
|
||||
"new_password": "Yeni Şifre",
|
||||
"notifier_modal": "{ type, select, true {Edit} false {Create} other {Other}} Bildirici",
|
||||
"notifiers": "Bildirimde Bulunanlar",
|
||||
"test": "Test",
|
||||
"theme_settings": "Tema Ayarları",
|
||||
"theme_settings_sub": "Tema ayarları tarayıcınızın yerel depolama alanında saklanır. Temayı istediğiniz zaman değiştirebilirsiniz. \nTemanızı ayarlamakta sorun yaşıyorsanız, tarayıcınızı yenilemeyi deneyin.",
|
||||
"update_group": "Grubu Güncelle",
|
||||
"notifiers_sub": "Yaklaşan bakım hatırlatmaları için bildirimler alın"
|
||||
}
|
||||
}
|
||||
128
frontend/locales/zh_Hans.json
Normal file
128
frontend/locales/zh_Hans.json
Normal file
@@ -0,0 +1,128 @@
|
||||
{
|
||||
"components": {
|
||||
"app": {
|
||||
"import_dialog": {
|
||||
"title": "导入 CSV 文件",
|
||||
"upload": "上传",
|
||||
"description": "导入包含物品、标签和位置的CSV文件。更多有关信息,\n请参阅文档查看所需格式。",
|
||||
"change_warning": "导入行为会导致现有的 import_refs 的字段被覆盖。\n如果 CSV 文件中存在 import_ref,则将使用 CSV 文件中的值更新该项。"
|
||||
}
|
||||
},
|
||||
"item": {
|
||||
"create_modal": {
|
||||
"title": "创建物品",
|
||||
"photo_button": "照片 📷"
|
||||
},
|
||||
"view": {
|
||||
"selectable": {
|
||||
"card": "卡片模式",
|
||||
"items": "物品",
|
||||
"no_items": "没有物品可用展示",
|
||||
"table": "表格模式"
|
||||
}
|
||||
}
|
||||
},
|
||||
"location": {
|
||||
"create_modal": {
|
||||
"title": "创建位置"
|
||||
}
|
||||
},
|
||||
"global": {
|
||||
"page_qr_code": {
|
||||
"page_url": "页面URL"
|
||||
},
|
||||
"password_score": {
|
||||
"password_strength": "密码强度"
|
||||
}
|
||||
},
|
||||
"label": {
|
||||
"create_modal": {
|
||||
"title": "创建标签"
|
||||
}
|
||||
}
|
||||
},
|
||||
"global": {
|
||||
"create_and_add": "保存并继续创建",
|
||||
"email": "邮箱",
|
||||
"items": "物品",
|
||||
"build": "编译:{build}",
|
||||
"confirm": "确认",
|
||||
"create": "创建",
|
||||
"created": "已创建",
|
||||
"follow_dev": "关注开发者",
|
||||
"github": "Github项目",
|
||||
"join_discord": "加入Discord讨论",
|
||||
"labels": "标签",
|
||||
"sign_out": "注销",
|
||||
"version": "版本:{version}",
|
||||
"welcome": "欢迎,{username}",
|
||||
"name": "名称",
|
||||
"password": "密码",
|
||||
"read_docs": "查阅文档",
|
||||
"search": "搜索",
|
||||
"submit": "提交",
|
||||
"locations": "位置"
|
||||
},
|
||||
"index": {
|
||||
"disabled_registration": "已禁用注册",
|
||||
"dont_join_group": "不想加入群组?",
|
||||
"joining_group": "您正在加入现有群组!",
|
||||
"login": "登录",
|
||||
"register": "注册",
|
||||
"set_name": "你的名称叫什么?",
|
||||
"remember_me": "记住我",
|
||||
"set_email": "您的电子邮箱是什么?",
|
||||
"tagline": "跟踪、整理和管理您的物品。",
|
||||
"set_password": "设置密码"
|
||||
},
|
||||
"items": {
|
||||
"last": "最后一页",
|
||||
"negate_labels": "取消选中的标签",
|
||||
"next_page": "下一页",
|
||||
"no_results": "没有可显示的物品",
|
||||
"options": "选项",
|
||||
"prev_page": "上一页",
|
||||
"add": "添加",
|
||||
"field_selector": "字段选择",
|
||||
"first": "第一页",
|
||||
"order_by": "排序方式",
|
||||
"pages": "第{page}页,共{totalPages}页",
|
||||
"query_id": "查询资产ID: {id}",
|
||||
"created_at": "创建于",
|
||||
"custom_fields": "自定义字段",
|
||||
"field_value": "字段值",
|
||||
"include_archive": "包括已存档的项目",
|
||||
"reset_search": "重置搜索",
|
||||
"results": "{ total } 条结果",
|
||||
"tip_1": "位置和标签过滤器使用“或”操作。如果选择了多个位置或标签,\n则只需要任意一个匹配上即可。",
|
||||
"tip_2": "以“#”为前缀的搜索将变成查询资产ID(例如“#000-001” )",
|
||||
"tip_3": "字段过滤器使用“或”操作。如果选择了多个位置或标签,\n则只需要任意一个匹配上即可。",
|
||||
"tips": "建议",
|
||||
"tips_sub": "搜索提示",
|
||||
"updated_at": "更新于"
|
||||
},
|
||||
"profile": {
|
||||
"group_settings_sub": "共享组设置。您可能需要刷新浏览器来让某些设置生效。",
|
||||
"notifier_modal": "{ type, select, true {编辑} false {创建} other {Other}} 通知器",
|
||||
"active": "活跃",
|
||||
"delete_account_sub": "删除您的帐户及其所有相关数据。这是无法撤消的。",
|
||||
"inactive": "非活跃",
|
||||
"theme_settings_sub": "主题设置存储在浏览器的本地存储中。您可以随时更改主题。\n如果您在设置主题时遇到问题,请尝试刷新浏览器。",
|
||||
"update_group": "更新组",
|
||||
"change_password": "更改密码",
|
||||
"currency_format": "货币格式",
|
||||
"current_password": "原密码",
|
||||
"delete_account": "删除帐户",
|
||||
"enabled": "已启用",
|
||||
"gen_invite": "生成邀请链接",
|
||||
"group_settings": "组设置",
|
||||
"new_password": "新密码",
|
||||
"notifiers": "通知器",
|
||||
"notifiers_sub": "获取即将到来的维护提醒通知",
|
||||
"test": "测试",
|
||||
"theme_settings": "主题设置",
|
||||
"url": "网址",
|
||||
"user_profile": "用户资料",
|
||||
"user_profile_sub": "邀请用户共同管理您的资产。"
|
||||
}
|
||||
}
|
||||
128
frontend/locales/zh_Hant.json
Normal file
128
frontend/locales/zh_Hant.json
Normal file
@@ -0,0 +1,128 @@
|
||||
{
|
||||
"items": {
|
||||
"include_archive": "包括存檔項目",
|
||||
"tip_1": "位置和標籤過濾器使用“OR”運算。如果選擇了多個,則一次配對\n只會使用一個。",
|
||||
"pages": "第{ page } 頁(共{ totalPages })",
|
||||
"tip_3": "字段過濾器使用“OR”運算。如果選擇了多個,則一次配對只需要選擇\n一個。",
|
||||
"field_value": "欄位值",
|
||||
"field_selector": "欄位選擇器",
|
||||
"custom_fields": "自訂欄位",
|
||||
"first": "第一項",
|
||||
"add": "添加",
|
||||
"created_at": "創建於",
|
||||
"last": "最後一項",
|
||||
"negate_labels": "取消選定的標籤",
|
||||
"next_page": "下一頁",
|
||||
"no_results": "沒有找到項目",
|
||||
"options": "選項",
|
||||
"order_by": "排序",
|
||||
"prev_page": "上一頁",
|
||||
"query_id": "查詢資產ID號碼:{ id }",
|
||||
"reset_search": "重置搜尋",
|
||||
"results": "{ total }結果",
|
||||
"tips": "提示",
|
||||
"tips_sub": "搜尋技巧",
|
||||
"updated_at": "更新於",
|
||||
"tip_2": "以「#」為前綴的搜尋將查詢資產 ID(例如「#000-001」)"
|
||||
},
|
||||
"components": {
|
||||
"app": {
|
||||
"import_dialog": {
|
||||
"description": "匯入包含您的商品、標籤和位置的 CSV 檔案。有關所需格式的更多信息\n,請參閱文件。",
|
||||
"title": "匯入CSV檔案",
|
||||
"upload": "上傳",
|
||||
"change_warning": "現有 import_refs 的導入行為已變更。如果 CSV 檔案中存在 import_ref,則\n項目將使用 CSV 檔案中的值進行更新。"
|
||||
}
|
||||
},
|
||||
"global": {
|
||||
"page_qr_code": {
|
||||
"page_url": "頁面網址"
|
||||
},
|
||||
"password_score": {
|
||||
"password_strength": "密碼強度"
|
||||
}
|
||||
},
|
||||
"item": {
|
||||
"create_modal": {
|
||||
"title": "新增項目",
|
||||
"photo_button": "相片 📷"
|
||||
},
|
||||
"view": {
|
||||
"selectable": {
|
||||
"card": "卡牌風",
|
||||
"items": "項目",
|
||||
"no_items": "沒有可顯示的項目",
|
||||
"table": "表格"
|
||||
}
|
||||
}
|
||||
},
|
||||
"label": {
|
||||
"create_modal": {
|
||||
"title": "新增標籤"
|
||||
}
|
||||
},
|
||||
"location": {
|
||||
"create_modal": {
|
||||
"title": "新增地點"
|
||||
}
|
||||
}
|
||||
},
|
||||
"global": {
|
||||
"build": "建立{ build }",
|
||||
"confirm": "確認",
|
||||
"create": "新增",
|
||||
"create_and_add": "創建並添加另一個",
|
||||
"created": "已建立",
|
||||
"email": "電子信箱",
|
||||
"github": "GitHub項目",
|
||||
"items": "項目",
|
||||
"join_discord": "加入Discord",
|
||||
"labels": "標籤",
|
||||
"locations": "地點",
|
||||
"name": "名字",
|
||||
"password": "密碼",
|
||||
"read_docs": "閱讀文件",
|
||||
"search": "搜尋",
|
||||
"sign_out": "登出",
|
||||
"submit": "提交",
|
||||
"version": "版本:{ version }",
|
||||
"follow_dev": "關注開發者",
|
||||
"welcome": "歡迎,{ username }"
|
||||
},
|
||||
"index": {
|
||||
"disabled_registration": "已禁用註冊",
|
||||
"dont_join_group": "不想加入群組?",
|
||||
"joining_group": "您正在加入現有群組!",
|
||||
"login": "登入",
|
||||
"register": "註冊",
|
||||
"remember_me": "記住帳號",
|
||||
"set_email": "你的電子郵件是什麼?",
|
||||
"set_name": "你叫什麼名字?",
|
||||
"tagline": "追蹤、組織和管理您的物品。",
|
||||
"set_password": "設定您的密碼"
|
||||
},
|
||||
"profile": {
|
||||
"change_password": "變更密碼",
|
||||
"currency_format": "貨幣格式",
|
||||
"current_password": "當前密碼",
|
||||
"delete_account": "刪除帳戶",
|
||||
"enabled": "啟用",
|
||||
"gen_invite": "產生邀請連結",
|
||||
"group_settings": "群組設定",
|
||||
"group_settings_sub": "共享群組設定。您可能需要刷新瀏覽器才能套用設定。",
|
||||
"inactive": "不活躍",
|
||||
"new_password": "新密碼",
|
||||
"active": "活躍",
|
||||
"delete_account_sub": "刪除您的帳戶及其所有相關資料。此操作無法撤回。",
|
||||
"notifiers": "通知者",
|
||||
"notifiers_sub": "獲取即將到來的維護提醒的通知",
|
||||
"test": "測試",
|
||||
"theme_settings": "主題設定",
|
||||
"update_group": "更新群組",
|
||||
"url": "網址",
|
||||
"user_profile": "使用者資料",
|
||||
"user_profile_sub": "邀請使用者並管理您的帳戶。",
|
||||
"theme_settings_sub": "主題設定儲存在瀏覽器的本機儲存中。您可以隨時變更主題。如果你是\n 設定主題時遇到問題,請嘗試刷新瀏覽器。",
|
||||
"notifier_modal": "通知 { type, select, true {Edit} false {Create} other {Other}}"
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,9 @@ import { defineNuxtConfig } from "nuxt/config";
|
||||
// https://v3.nuxtjs.org/api/configuration/nuxt.config
|
||||
export default defineNuxtConfig({
|
||||
ssr: false,
|
||||
build: {
|
||||
transpile: ["vue-i18n"],
|
||||
},
|
||||
modules: [
|
||||
"@nuxtjs/tailwindcss",
|
||||
"@pinia/nuxt",
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
"devDependencies": {
|
||||
"@faker-js/faker": "^8.0.0",
|
||||
"@iconify-json/mdi": "^1.1.64",
|
||||
"@intlify/unplugin-vue-i18n": "^4.0.0",
|
||||
"@nuxtjs/eslint-config-typescript": "^12.0.0",
|
||||
"@types/dompurify": "^3.0.0",
|
||||
"@types/markdown-it": "^13.0.0",
|
||||
@@ -27,13 +28,15 @@
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"eslint-plugin-vue": "^9.4.0",
|
||||
"h3": "^1.7.1",
|
||||
"intl-messageformat": "^10.5.14",
|
||||
"isomorphic-fetch": "^3.0.0",
|
||||
"nuxt": "3.6.5",
|
||||
"prettier": "^3.2.5",
|
||||
"typescript": "^5.0.0",
|
||||
"unplugin-icons": "^0.18.5",
|
||||
"vite-plugin-eslint": "^1.8.1",
|
||||
"vitest": "^1.0.0"
|
||||
"vitest": "^1.0.0",
|
||||
"vue-i18n": "^9.13.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@headlessui/vue": "^1.7.9",
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import MdiGithub from "~icons/mdi/github";
|
||||
import MdiTwitter from "~icons/mdi/twitter";
|
||||
import MdiDiscord from "~icons/mdi/discord";
|
||||
import MdiFolder from "~icons/mdi/folder";
|
||||
import MdiAccount from "~icons/mdi/account";
|
||||
@@ -8,6 +7,7 @@
|
||||
import MdiLogin from "~icons/mdi/login";
|
||||
import MdiArrowRight from "~icons/mdi/arrow-right";
|
||||
import MdiLock from "~icons/mdi/lock";
|
||||
import MdiMastodon from '~icons/mdi/mastodon';
|
||||
|
||||
useHead({
|
||||
title: "Homebox | Organize and Tag Your Stuff",
|
||||
@@ -77,6 +77,15 @@
|
||||
|
||||
async function registerUser() {
|
||||
loading.value = true;
|
||||
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
|
||||
if (!emailRegex.test(email.value)) {
|
||||
toast.error("Invalid email address");
|
||||
loading.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const { error } = await api.register({
|
||||
name: username.value,
|
||||
email: email.value,
|
||||
@@ -149,19 +158,19 @@
|
||||
<AppLogo class="w-12 -mb-4" />
|
||||
x
|
||||
</h2>
|
||||
<p class="ml-1 text-lg text-base-content/50">Track, Organize, and Manage your Things.</p>
|
||||
<p class="ml-1 text-lg text-base-content/50">{{ $t("index.tagline") }}</p>
|
||||
</div>
|
||||
<div class="flex mt-6 sm:mt-0 gap-4 ml-auto text-neutral-content">
|
||||
<a class="tooltip" data-tip="Project Github" href="https://github.com/sysadminsmedia/homebox" target="_blank">
|
||||
<a class="tooltip" :data-tip="$t('global.github')" href="https://github.com/sysadminsmedia/homebox" target="_blank">
|
||||
<MdiGithub class="h-8 w-8" />
|
||||
</a>
|
||||
<a href="https://twitter.com/haybytes" class="tooltip" data-tip="Follow The Developer" target="_blank">
|
||||
<MdiTwitter class="h-8 w-8" />
|
||||
<a href="https://noc.social/@sysadminsmedia" class="tooltip" :data-tip="$t('global.follow_dev')" target="_blank">
|
||||
<MdiMastodon class="h-8 w-8" />
|
||||
</a>
|
||||
<a href="https://discord.gg/tuncmNrE4z" class="tooltip" data-tip="Join The Discord" target="_blank">
|
||||
<a href="https://discord.gg/aY4DCkpNA9" class="tooltip" :data-tip="$t('global.join_discord')" target="_blank">
|
||||
<MdiDiscord class="h-8 w-8" />
|
||||
</a>
|
||||
<a href="https://hay-kot.github.io/homebox/" class="tooltip" data-tip="Read The Docs" target="_blank">
|
||||
<a href="https://homebox.sysadminsmedia.com/en/" class="tooltip" :data-tip="$t('global.read_docs')" target="_blank">
|
||||
<MdiFolder class="h-8 w-8" />
|
||||
</a>
|
||||
</div>
|
||||
@@ -174,17 +183,17 @@
|
||||
<div class="card-body">
|
||||
<h2 class="card-title text-2xl align-center">
|
||||
<MdiAccount class="mr-1 w-7 h-7" />
|
||||
Register
|
||||
{{ $t("index.register") }}
|
||||
</h2>
|
||||
<FormTextField v-model="email" label="Set your email?" />
|
||||
<FormTextField v-model="username" label="What's your name?" />
|
||||
<FormTextField v-model="email" :label="$t('index.set_email')" />
|
||||
<FormTextField v-model="username" :label="$t('index.set_name')" />
|
||||
<div v-if="!(groupToken == '')" class="pt-4 pb-1 text-center">
|
||||
<p>You're Joining an Existing Group!</p>
|
||||
<p>{{ $t("index.joining_group") }}</p>
|
||||
<button type="button" class="text-xs underline" @click="groupToken = ''">
|
||||
Don't Want To Join a Group?
|
||||
{{ $t("index.dont_join_group") }}
|
||||
</button>
|
||||
</div>
|
||||
<FormPassword v-model="password" label="Set your password" />
|
||||
<FormPassword v-model="password" :label="$t('index.set_password')" />
|
||||
<PasswordScore v-model:valid="canRegister" :password="password" />
|
||||
<div class="card-actions justify-end">
|
||||
<button
|
||||
@@ -193,7 +202,7 @@
|
||||
:class="loading ? 'loading' : ''"
|
||||
:disabled="loading || !canRegister"
|
||||
>
|
||||
Register
|
||||
{{ $t("index.register") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -204,17 +213,17 @@
|
||||
<div class="card-body">
|
||||
<h2 class="card-title text-2xl align-center">
|
||||
<MdiAccount class="mr-1 w-7 h-7" />
|
||||
Login
|
||||
{{ $t("index.login") }}
|
||||
</h2>
|
||||
<template v-if="status && status.demo">
|
||||
<p class="text-xs italic text-center">This is a demo instance</p>
|
||||
<p class="text-xs text-center"><b>Email</b> demo@example.com</p>
|
||||
<p class="text-xs text-center"><b>Password</b> demo</p>
|
||||
<p class="text-xs text-center"><b>{{ $t("global.email") }}</b> demo@example.com</p>
|
||||
<p class="text-xs text-center"><b>{{ $t("global.password") }}</b> demo</p>
|
||||
</template>
|
||||
<FormTextField v-model="email" label="Email" />
|
||||
<FormPassword v-model="loginPassword" label="Password" />
|
||||
<FormTextField v-model="email" :label="$t('global.email')" />
|
||||
<FormPassword v-model="loginPassword" :label="$t('global.password')" />
|
||||
<div class="max-w-[140px]">
|
||||
<FormCheckbox v-model="remember" label="Remember Me" />
|
||||
<FormCheckbox v-model="remember" :label="$t('index.remember_me')" />
|
||||
</div>
|
||||
<div class="card-actions justify-end">
|
||||
<button
|
||||
@@ -223,7 +232,7 @@
|
||||
:class="loading ? 'loading' : ''"
|
||||
:disabled="loading"
|
||||
>
|
||||
Login
|
||||
{{ $t("index.login") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -241,18 +250,21 @@
|
||||
<MdiLogin v-else class="w-5 h-5 swap-off" />
|
||||
<MdiArrowRight class="w-5 h-5 swap-on" />
|
||||
</template>
|
||||
{{ registerForm ? "Login" : "Register" }}
|
||||
{{ registerForm ? $t("index.login") : $t("index.register") }}
|
||||
</BaseButton>
|
||||
<p v-else class="text-base-content italic text-sm inline-flex items-center gap-2">
|
||||
<MdiLock class="w-4 h-4 inline-block" />
|
||||
Registration Disabled
|
||||
{{ $t("disabled_registration") }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<footer v-if="status" class="mt-auto text-center w-full bottom-0 pb-4">
|
||||
<p class="text-center text-sm">Version: {{ status.build.version }} ~ Build: {{ status.build.commit }}</p>
|
||||
<p class="text-center text-sm">
|
||||
{{ $t("global.version", { version: status.build.version }) }} ~
|
||||
{{ $t("global.build", { build: status.build.commit }) }}
|
||||
</p>
|
||||
</footer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
const includeArchived = useRouteQuery("archived", false);
|
||||
const fieldSelector = useRouteQuery("fieldSelector", false);
|
||||
const negateLabels = useRouteQuery("negateLabels", false);
|
||||
const orderBy = useRouteQuery("orderBy", "name");
|
||||
|
||||
const totalPages = computed(() => Math.ceil(total.value / pageSize.value));
|
||||
const hasNext = computed(() => page.value * pageSize.value < total.value);
|
||||
@@ -169,6 +170,12 @@
|
||||
}
|
||||
});
|
||||
|
||||
watch(orderBy, (newV, oldV) => {
|
||||
if (newV !== oldV) {
|
||||
search();
|
||||
}
|
||||
});
|
||||
|
||||
async function fetchValues(field: string): Promise<string[]> {
|
||||
if (fieldValuesCache.value[field]) {
|
||||
return fieldValuesCache.value[field];
|
||||
@@ -201,6 +208,7 @@
|
||||
pageSize: pageSize.value,
|
||||
includeArchived: includeArchived.value ? "true" : "false",
|
||||
negateLabels: negateLabels.value ? "true" : "false",
|
||||
orderBy: orderBy.value,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -231,6 +239,7 @@
|
||||
includeArchived: includeArchived.value,
|
||||
page: page.value,
|
||||
pageSize: pageSize.value,
|
||||
orderBy: orderBy.value,
|
||||
fields,
|
||||
});
|
||||
|
||||
@@ -278,6 +287,7 @@
|
||||
archived: includeArchived.value ? "true" : "false",
|
||||
fieldSelector: fieldSelector.value ? "true" : "false",
|
||||
negateLabels: negateLabels.value ? "true" : "false",
|
||||
orderBy: orderBy.value,
|
||||
pageSize: pageSize.value,
|
||||
page: page.value,
|
||||
q: query.value,
|
||||
@@ -311,6 +321,7 @@
|
||||
fieldSelector: "false",
|
||||
pageSize: 10,
|
||||
page: 1,
|
||||
orderBy: "name",
|
||||
q: "",
|
||||
loc: [],
|
||||
lab: [],
|
||||
@@ -327,9 +338,9 @@
|
||||
<div v-if="locations && labels">
|
||||
<div class="flex flex-wrap md:flex-nowrap gap-4 items-end">
|
||||
<div class="w-full">
|
||||
<FormTextField v-model="query" placeholder="Search" />
|
||||
<FormTextField v-model="query" :placeholder="$t('global.search')" />
|
||||
<div v-if="byAssetId" class="text-sm pl-2 pt-2">
|
||||
<p>Querying Asset ID Number: {{ parsedAssetId }}</p>
|
||||
<p>{{ $t("items.query_id", { id: parsedAssetId }) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<BaseButton class="btn-block md:w-auto" @click.prevent="submit">
|
||||
@@ -337,12 +348,12 @@
|
||||
<MdiLoading v-if="loading" class="animate-spin" />
|
||||
<MdiMagnify v-else />
|
||||
</template>
|
||||
Search
|
||||
{{ $t("global.search") }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap md:flex-nowrap gap-2 w-full py-2">
|
||||
<SearchFilter v-model="selectedLocations" label="Locations" :options="locationFlatTree">
|
||||
<SearchFilter v-model="selectedLocations" :label="$t('global.locations')" :options="locationFlatTree">
|
||||
<template #display="{ item }">
|
||||
<div>
|
||||
<div class="flex w-full">
|
||||
@@ -354,52 +365,60 @@
|
||||
</div>
|
||||
</template>
|
||||
</SearchFilter>
|
||||
<SearchFilter v-model="selectedLabels" label="Labels" :options="labels" />
|
||||
<SearchFilter v-model="selectedLabels" :label="$t('global.labels')" :options="labels" />
|
||||
<div class="dropdown">
|
||||
<label tabindex="0" class="btn btn-xs">Options</label>
|
||||
<label tabindex="0" class="btn btn-xs">{{ $t("items.options") }}</label>
|
||||
<div
|
||||
tabindex="0"
|
||||
class="dropdown-content mt-1 max-h-72 p-4 w-64 overflow-auto shadow bg-base-100 rounded-md -translate-x-24"
|
||||
>
|
||||
<label class="label cursor-pointer mr-auto">
|
||||
<input v-model="includeArchived" type="checkbox" class="toggle toggle-sm toggle-primary" />
|
||||
<span class="label-text ml-4"> Include Archived Items </span>
|
||||
<span class="label-text ml-4"> {{ $t("items.include_archive") }} </span>
|
||||
</label>
|
||||
<label class="label cursor-pointer mr-auto">
|
||||
<input v-model="fieldSelector" type="checkbox" class="toggle toggle-sm toggle-primary" />
|
||||
<span class="label-text ml-4"> Field Selector </span>
|
||||
<span class="label-text ml-4"> {{ $t("items.field_selector") }} </span>
|
||||
</label>
|
||||
<label class="label cursor-pointer mr-auto">
|
||||
<input v-model="negateLabels" type="checkbox" class="toggle toggle-sm toggle-primary" />
|
||||
<span class="label-text ml-4"> Negate selected labels </span>
|
||||
<span class="label-text ml-4"> {{ $t("items.negate_labels") }} </span>
|
||||
</label>
|
||||
<label class="label cursor-pointer mr-auto">
|
||||
<select v-model="orderBy" class="select select-bordered select-sm">
|
||||
<option value="name" selected>{{ $t("global.name") }}</option>
|
||||
<option value="createdAt">{{ $t("items.created_at") }}</option>
|
||||
<option value="updatedAt">{{ $t("items.updated_at") }}</option>
|
||||
</select>
|
||||
<span class="label-text ml-4"> {{ $t("items.order_by") }} </span>
|
||||
</label>
|
||||
<hr class="my-2" />
|
||||
<BaseButton class="btn-block btn-sm" @click="reset"> Reset Search</BaseButton>
|
||||
<BaseButton class="btn-block btn-sm" @click="reset"> {{ $t("items.reset_search") }} </BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dropdown ml-auto dropdown-end">
|
||||
<label tabindex="0" class="btn btn-xs">Tips</label>
|
||||
<label tabindex="0" class="btn btn-xs">{{ $t("items.tips") }}</label>
|
||||
<div
|
||||
tabindex="0"
|
||||
class="dropdown-content mt-1 p-4 w-[325px] text-sm overflow-auto shadow bg-base-100 rounded-md"
|
||||
>
|
||||
<p class="text-base">Search Tips</p>
|
||||
<p class="text-base">{{ $t("items.tips_sub") }}</p>
|
||||
<ul class="mt-1 list-disc pl-6">
|
||||
<li>
|
||||
Location and label filters use the 'OR' operation. If more than one is selected only one will be
|
||||
required for a match.
|
||||
{{ $t("items.tip_1") }}
|
||||
</li>
|
||||
<li>Searches prefixed with '#'' will query for a asset ID (example '#000-001')</li>
|
||||
<li>
|
||||
Field filters use the 'OR' operation. If more than one is selected only one will be required for a
|
||||
match.
|
||||
{{ $t("items.tip_2") }}
|
||||
</li>
|
||||
<li>
|
||||
{{ $t("items.tip_3") }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="fieldSelector" class="py-4 space-y-2">
|
||||
<p>Custom Fields</p>
|
||||
<p>{{ $t("items.custom_fields") }}</p>
|
||||
<div v-for="(f, idx) in fieldTuples" :key="idx" class="flex flex-wrap gap-2">
|
||||
<div class="form-control w-full max-w-xs">
|
||||
<label class="label">
|
||||
@@ -416,7 +435,7 @@
|
||||
</div>
|
||||
<div class="form-control w-full max-w-xs">
|
||||
<label class="label">
|
||||
<span class="label-text">Field Value</span>
|
||||
<span class="label-text">{{ $t("items.field_value") }}</span>
|
||||
</label>
|
||||
<select v-model="fieldTuples[idx][1]" class="select-bordered select" :items="fieldValuesCache[f[0]]">
|
||||
<option v-for="v in fieldValuesCache[f[0]]" :key="v" :value="v">{{ v }}</option>
|
||||
@@ -430,38 +449,42 @@
|
||||
<MdiDelete class="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
<BaseButton type="button" class="btn-sm mt-2" @click="() => fieldTuples.push(['', ''])"> Add</BaseButton>
|
||||
<BaseButton type="button" class="btn-sm mt-2" @click="() => fieldTuples.push(['', ''])">
|
||||
{{ $t("items.add") }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="mt-10">
|
||||
<BaseSectionHeader ref="itemsTitle"> Items </BaseSectionHeader>
|
||||
<BaseSectionHeader ref="itemsTitle"> {{ $t("global.items") }} </BaseSectionHeader>
|
||||
<p class="text-base font-medium flex items-center">
|
||||
{{ total }} Results
|
||||
<span class="text-base ml-auto"> Page {{ page }} of {{ totalPages }}</span>
|
||||
{{ $t("items.results", { total: total }) }}
|
||||
<span class="text-base ml-auto"> {{ $t("items.pages", { page: page, totalPages: totalPages }) }} </span>
|
||||
</p>
|
||||
|
||||
<div ref="cardgrid" class="grid mt-4 grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
|
||||
<ItemCard v-for="item in items" :key="item.id" :item="item" />
|
||||
|
||||
<div class="hidden first:inline text-xl">No Items Found</div>
|
||||
<div class="hidden first:inline text-xl">{{ $t("items.no_results") }}</div>
|
||||
</div>
|
||||
<div v-if="items.length > 0 && (hasNext || hasPrev)" class="mt-10 flex gap-2 flex-col items-center">
|
||||
<div class="flex">
|
||||
<div class="btn-group">
|
||||
<button :disabled="!hasPrev" class="btn text-no-transform" @click="prev">
|
||||
<MdiChevronLeft class="mr-1 h-6 w-6" name="mdi-chevron-left" />
|
||||
Prev
|
||||
{{ $t("items.prev_page") }}
|
||||
</button>
|
||||
<button v-if="hasPrev" class="btn text-no-transform" @click="page = 1">{{ $t("items.first") }}</button>
|
||||
<button v-if="hasNext" class="btn text-no-transform" @click="page = totalPages">
|
||||
{{ $t("items.last") }}
|
||||
</button>
|
||||
<button v-if="hasPrev" class="btn text-no-transform" @click="page = 1">First</button>
|
||||
<button v-if="hasNext" class="btn text-no-transform" @click="page = totalPages">Last</button>
|
||||
<button :disabled="!hasNext" class="btn text-no-transform" @click="next">
|
||||
Next
|
||||
{{ $t("items.next_page") }}
|
||||
<MdiChevronRight class="ml-1 h-6 w-6" name="mdi-chevron-right" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-sm font-bold">Page {{ page }} of {{ totalPages }}</p>
|
||||
<p class="text-sm font-bold">{{ $t("items.pages", { page: page, totalPages: totalPages }) }}</p>
|
||||
</div>
|
||||
</section>
|
||||
</BaseContainer>
|
||||
|
||||
@@ -89,13 +89,6 @@
|
||||
notify.success("Group updated");
|
||||
}
|
||||
|
||||
const pubApi = usePublicApi();
|
||||
const { data: status } = useAsyncData(async () => {
|
||||
const { data } = await pubApi.status();
|
||||
|
||||
return data;
|
||||
});
|
||||
|
||||
const { setTheme } = useTheme();
|
||||
|
||||
const auth = useAuthContext();
|
||||
@@ -305,11 +298,11 @@
|
||||
<template>
|
||||
<div>
|
||||
<BaseModal v-model="passwordChange.dialog">
|
||||
<template #title> Change Password </template>
|
||||
<template #title> {{ $t("profile.change_password") }} </template>
|
||||
|
||||
<form @submit.prevent="changePassword">
|
||||
<FormPassword v-model="passwordChange.current" label="Current Password" placeholder="" />
|
||||
<FormPassword v-model="passwordChange.new" label="New Password" placeholder="" />
|
||||
<FormPassword v-model="passwordChange.current" :label="$t('profile.current_password')" placeholder="" />
|
||||
<FormPassword v-model="passwordChange.new" :label="$t('profile.new_password')" placeholder="" />
|
||||
<PasswordScore v-model:valid="passwordChange.isValid" :password="passwordChange.new" />
|
||||
|
||||
<div class="flex">
|
||||
@@ -319,26 +312,26 @@
|
||||
:disabled="!passwordChange.isValid"
|
||||
type="submit"
|
||||
>
|
||||
Submit
|
||||
{{ $t("global.submit") }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
</form>
|
||||
</BaseModal>
|
||||
|
||||
<BaseModal v-model="notifierDialog">
|
||||
<template #title> {{ notifier ? "Edit" : "Create" }} Notifier </template>
|
||||
<template #title> {{ $t("profile.notifier_modal", {type: (notifier != null)}) }} </template>
|
||||
|
||||
<form @submit.prevent="createNotifier">
|
||||
<template v-if="notifier">
|
||||
<FormTextField v-model="notifier.name" label="Name" />
|
||||
<FormTextField v-model="notifier.url" label="URL" />
|
||||
<FormTextField v-model="notifier.name" :label="$t('global.name')" />
|
||||
<FormTextField v-model="notifier.url" :label="$t('profile.url')" />
|
||||
<div class="max-w-[100px]">
|
||||
<FormCheckbox v-model="notifier.isActive" label="Enabled" />
|
||||
<FormCheckbox v-model="notifier.isActive" :label="$t('profile.enabled')" />
|
||||
</div>
|
||||
</template>
|
||||
<div class="flex gap-2 justify-between mt-4">
|
||||
<BaseButton :disabled="!(notifier && notifier.url)" type="button" @click="testNotifier"> Test </BaseButton>
|
||||
<BaseButton type="submit"> Submit </BaseButton>
|
||||
<BaseButton :disabled="!(notifier && notifier.url)" type="button" @click="testNotifier"> {{ $t("profile.test") }} </BaseButton>
|
||||
<BaseButton type="submit"> {{ $t("global.submit") }} </BaseButton>
|
||||
</div>
|
||||
</form>
|
||||
</BaseModal>
|
||||
@@ -348,8 +341,8 @@
|
||||
<template #title>
|
||||
<BaseSectionHeader>
|
||||
<MdiAccount class="mr-2 -mt-1 text-base-600" />
|
||||
<span class="text-base-600"> User Profile </span>
|
||||
<template #description> Invite users, and manage your account. </template>
|
||||
<span class="text-base-600"> {{ $t("profile.user_profile") }} </span>
|
||||
<template #description> {{ $t("profile.user_profile_sub") }} </template>
|
||||
</BaseSectionHeader>
|
||||
</template>
|
||||
|
||||
@@ -357,8 +350,8 @@
|
||||
|
||||
<div class="p-4">
|
||||
<div class="flex gap-2">
|
||||
<BaseButton size="sm" @click="openPassChange"> Change Password </BaseButton>
|
||||
<BaseButton size="sm" @click="generateToken"> Generate Invite Link </BaseButton>
|
||||
<BaseButton size="sm" @click="openPassChange"> {{ $t("profile.change_password") }} </BaseButton>
|
||||
<BaseButton size="sm" @click="generateToken"> {{ $t("profile.gen_invite") }} </BaseButton>
|
||||
</div>
|
||||
<div v-if="token" class="pt-4 flex items-center pl-1">
|
||||
<CopyText class="mr-2 btn-primary btn btn-outline btn-square btn-sm" :text="tokenUrl" />
|
||||
@@ -375,8 +368,8 @@
|
||||
<template #title>
|
||||
<BaseSectionHeader>
|
||||
<MdiMegaphone class="mr-2 -mt-1 text-base-600" />
|
||||
<span class="text-base-600"> Notifiers </span>
|
||||
<template #description> Get notifications for up coming maintenance reminders </template>
|
||||
<span class="text-base-600"> {{ $t("profile.notifiers") }} </span>
|
||||
<template #description> {{ $t("profile.notifiers_sub") }} </template>
|
||||
</BaseSectionHeader>
|
||||
</template>
|
||||
|
||||
@@ -399,11 +392,11 @@
|
||||
</div>
|
||||
<div class="flex justify-between py-1 flex-wrap text-sm">
|
||||
<p>
|
||||
<span v-if="n.isActive" class="badge badge-success"> Active </span>
|
||||
<span v-else class="badge badge-error"> Inactive</span>
|
||||
<span v-if="n.isActive" class="badge badge-success"> {{ $t("profile.active") }} </span>
|
||||
<span v-else class="badge badge-error"> {{ $t("profile.inactive") }} </span>
|
||||
</p>
|
||||
<p>
|
||||
Created
|
||||
{{ $t("global.created") }}
|
||||
<DateTime format="relative" datetime-type="time" :date="n.createdAt" />
|
||||
</p>
|
||||
</div>
|
||||
@@ -411,7 +404,7 @@
|
||||
</div>
|
||||
|
||||
<div class="p-4">
|
||||
<BaseButton size="sm" @click="openNotifierDialog"> Create </BaseButton>
|
||||
<BaseButton size="sm" @click="openNotifierDialog"> {{ $t("global.create") }} </BaseButton>
|
||||
</div>
|
||||
</BaseCard>
|
||||
|
||||
@@ -419,19 +412,19 @@
|
||||
<template #title>
|
||||
<BaseSectionHeader class="pb-0">
|
||||
<MdiAccountMultiple class="mr-2 -mt-1 text-base-600" />
|
||||
<span class="text-base-600"> Group Settings </span>
|
||||
<span class="text-base-600"> {{ $t("profile.group_settings") }} </span>
|
||||
<template #description>
|
||||
Shared Group Settings. You may need to refresh your browser for some settings to apply.
|
||||
{{ $t("profile.group_settings_sub") }}
|
||||
</template>
|
||||
</BaseSectionHeader>
|
||||
</template>
|
||||
|
||||
<div v-if="group && currencies && currencies.length > 0" class="p-5 pt-0">
|
||||
<FormSelect v-model="currency" label="Currency Format" :items="currencies" />
|
||||
<FormSelect v-model="currency" :label="$t('profile.currency_format')" :items="currencies" />
|
||||
<p class="m-2 text-sm">Example: {{ currencyExample }}</p>
|
||||
|
||||
<div class="mt-4">
|
||||
<BaseButton size="sm" @click="updateGroup"> Update Group </BaseButton>
|
||||
<BaseButton size="sm" @click="updateGroup"> {{ $t("profile.update_group") }} </BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
</BaseCard>
|
||||
@@ -440,10 +433,9 @@
|
||||
<template #title>
|
||||
<BaseSectionHeader>
|
||||
<MdiFill class="mr-2 text-base-600" />
|
||||
<span class="text-base-600"> Theme Settings </span>
|
||||
<span class="text-base-600"> {{ $t("profile.theme_settings") }} </span>
|
||||
<template #description>
|
||||
Theme settings are stored in your browser's local storage. You can change the theme at any time. If you're
|
||||
having trouble setting your theme try refreshing your browser.
|
||||
{{ $t("profile.theme_settings_sub") }}
|
||||
</template>
|
||||
</BaseSectionHeader>
|
||||
</template>
|
||||
@@ -491,18 +483,15 @@
|
||||
<template #title>
|
||||
<BaseSectionHeader>
|
||||
<MdiDelete class="mr-2 -mt-1 text-base-600" />
|
||||
<span class="text-base-600"> Delete Account</span>
|
||||
<template #description> Delete your account and all its associated data. </template>
|
||||
<span class="text-base-600"> {{ $t("profile.delete_account") }} </span>
|
||||
<template #description> {{ $t("profile.delete_account_sub") }} </template>
|
||||
</BaseSectionHeader>
|
||||
</template>
|
||||
<div class="p-4 px-6 border-t-2 border-gray-300">
|
||||
<BaseButton size="sm" class="btn-error" @click="deleteProfile"> Delete Account </BaseButton>
|
||||
<BaseButton size="sm" class="btn-error" @click="deleteProfile"> {{ $t("profile.delete_account") }} </BaseButton>
|
||||
</div>
|
||||
</BaseCard>
|
||||
</BaseContainer>
|
||||
<footer v-if="status" class="text-center w-full bottom-0 pb-4">
|
||||
<p class="text-center text-sm">Version: {{ status.build.version }} ~ Build: {{ status.build.commit }}</p>
|
||||
</footer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -308,7 +308,7 @@
|
||||
</p>
|
||||
<p>
|
||||
This feature is in early development stages and may change in future releases, if you have feedback please
|
||||
provide it in the <a href="https://github.com/sysadminsmedia/homebox/discussions/273">GitHub Discussion</a>
|
||||
provide it in the <a href="https://github.com/sysadminsmedia/homebox/discussions/53">GitHub Discussion</a>
|
||||
</p>
|
||||
<h2>Tips</h2>
|
||||
<ul>
|
||||
|
||||
@@ -42,8 +42,9 @@
|
||||
<div class="border-t px-6 pb-3 border-gray-300 divide-gray-300 divide-y">
|
||||
<DetailAction @action="modals.import = true">
|
||||
<template #title>Import Inventory</template>
|
||||
Imports the standard CSV format for Homebox. This will <b>not</b> overwrite any existing items in your
|
||||
inventory. It will only add new items.
|
||||
Imports the standard CSV format for Homebox. Without an <code>HB.import_ref</code> column, this will
|
||||
<b>not</b> overwrite any existing items in your inventory, only add new items. Rows with an
|
||||
<code>HB.import_ref</code> column are merged into existing items with the same import_ref, if one exists.
|
||||
</DetailAction>
|
||||
<DetailAction @action="getExportCSV()">
|
||||
<template #title>Export Inventory</template>
|
||||
@@ -104,7 +105,7 @@
|
||||
middleware: ["auth"],
|
||||
});
|
||||
useHead({
|
||||
title: "Homebox | Profile",
|
||||
title: "Homebox | Tools",
|
||||
});
|
||||
|
||||
const modals = ref({
|
||||
|
||||
63
frontend/plugins/i18n.ts
Normal file
63
frontend/plugins/i18n.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import type { CompileError, MessageCompiler, MessageContext } from "vue-i18n";
|
||||
import { createI18n } from "vue-i18n";
|
||||
import { IntlMessageFormat } from "intl-messageformat";
|
||||
|
||||
export default defineNuxtPlugin(({ vueApp }) => {
|
||||
function checkDefaultLanguage() {
|
||||
let matched = null;
|
||||
const languages = Object.getOwnPropertyNames(messages())
|
||||
languages.forEach(lang => {
|
||||
if (lang === navigator.language.replace('-', '_')) {
|
||||
matched = lang;
|
||||
}
|
||||
});
|
||||
if (!matched) {
|
||||
languages.forEach(lang => {
|
||||
const languagePartials = navigator.language.split('-')[0]
|
||||
if (lang === languagePartials) {
|
||||
matched = lang;
|
||||
}
|
||||
});
|
||||
}
|
||||
return matched;
|
||||
}
|
||||
const i18n = createI18n({
|
||||
legacy: false,
|
||||
globalInjection: true,
|
||||
locale: checkDefaultLanguage() || "en",
|
||||
fallbackLocale: "en",
|
||||
messageCompiler,
|
||||
messages: messages(),
|
||||
});
|
||||
vueApp.use(i18n);
|
||||
});
|
||||
|
||||
export const messageCompiler: MessageCompiler = (message, { locale, key, onError }) => {
|
||||
if (typeof message === "string") {
|
||||
/**
|
||||
* You can tune your message compiler performance more with your cache strategy or also memoization at here
|
||||
*/
|
||||
const formatter = new IntlMessageFormat(message, locale);
|
||||
return (ctx: MessageContext) => {
|
||||
return formatter.format(ctx.values);
|
||||
};
|
||||
} else {
|
||||
/**
|
||||
* for AST.
|
||||
* If you would like to support it,
|
||||
* You need to transform locale messages such as `json`, `yaml`, etc. with the bundle plugin.
|
||||
*/
|
||||
onError && onError(new Error("not support for AST") as CompileError);
|
||||
return () => key;
|
||||
}
|
||||
};
|
||||
|
||||
export const messages: Object = () => {
|
||||
let messages = {};
|
||||
const modules = import.meta.glob('~//locales/**.json', { eager: true });
|
||||
for (const path in modules) {
|
||||
const key = path.slice(9, -5);
|
||||
messages[key] = modules[path];
|
||||
}
|
||||
return messages;
|
||||
};
|
||||
12434
frontend/pnpm-lock.yaml
generated
12434
frontend/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user